跪拜 Guibai
← Back to the summary

Node.js File Reading: From Callback Hell to Clean Async/Await

fs is Node's built-in file system module for reading, writing, and manipulating files and folders. JavaScript is a single-threaded language, and fs provides four approaches — synchronous blocking, callback-based asynchronous, Promise, and async/await — that progressively solve the callback hell problem caused by nested asynchronous operations.

1. Two Ways to Import the Module

  1. Basic fs: provides synchronous APIs and callback-style asynchronous APIs
import fs from 'fs';
  1. fs/promises: specifically provides Promise-style APIs, compatible with async/await (recommended for production use)
import fs from 'fs/promises';

2. Approach 1: Synchronous Read with readFileSync (Blocks the Main Thread)

Characteristics: code executes sequentially; the entire thread is blocked while reading the file. Not suitable for endpoints or heavy I/O in loops; only suitable for one-time configuration file reads during project startup.

import fs from 'fs';
const syncData = fs.readFileSync('./test.txt', 'utf-8');
console.log(syncData);

Drawback: massive file reads and writes in concurrent scenarios severely degrade service performance.

3. Approach 2: Callback-Based Asynchronous readFile (ES6 Native Style, Callback Hell)

Node follows a unified error-first callback convention: (err, data) => {}

Reading multiple files serially produces deeply nested callbacks — callback hell — which is extremely costly to maintain.

import fs from 'fs';
fs.readFile('./file1.txt', 'utf-8', (err, data) => {
  if (!err) {
    console.log('file1', data);
  } else {
    console.log(err);
  }
  fs.readFile('./file2.txt', 'utf-8', (err, data) => {
    if (!err) {
      console.log('file2', data);
    } else {
      console.log(err);
    }
    fs.readFile('./file3.txt', 'utf-8', (err, data) => {
      if (!err) {
        console.log('file3', data);
      } else {
        console.log(err);
      }
    })
  })
})

4. Approach 3: Promise Chaining (Eliminates Nesting)

Based on fs/promises, readFile returns a Promise. Using .then() chains executes operations serially, solving the deep nesting problem. The drawback is that multi-file scenarios require repetitive .then() calls, making the code redundant.

import fs from 'fs/promises';
fs.readFile('./file1.txt', 'utf-8')
  .then(data => {
    console.log('file1', data);
    return fs.readFile('./file2.txt', 'utf-8');
  })
  .then(data => {
    console.log('file2', data);
    return fs.readFile('./file3.txt', 'utf-8');
  })
  .then(data => {
    console.log('file3', data);
  })

5. Approach 4: async/await (ES8 Optimal Solution, Top Choice for Production)

async/await is syntactic sugar over Promises. Under the hood it remains asynchronous and does not block the main thread; code is written linearly from top to bottom, offering the strongest readability. Limitation: await can only be used inside an async function; using await at the top level will throw an error. Wrap it in an IIFE (Immediately Invoked Function Expression). Supplement: Promises and await belong to microtasks; setTimeout belongs to macrotasks.

import fs from 'fs/promises';
(async () => {
  const file1Data = await fs.readFile('./file1.txt', 'utf-8');
  console.log('file1', file1Data);
  const file2Data = await fs.readFile('./file2.txt', 'utf-8');
  console.log('file2', file2Data);
  const file3Data = await fs.readFile('./file3.txt', 'utf-8');
  console.log('file3', file3Data);
})();

A complete, robust version (adding try/catch to capture read errors)

import fs from 'fs/promises';
(async () => {
  try {
    const data = await fs.readFile('./test.txt', 'utf-8');
    console.log(data);
  } catch (err) {
    console.error('File read failed:', err);
  }
})();

6. Asynchronous Code Evolution Path

Synchronous blocking (poor performance) → Callback functions (callback hell) → Promise then chaining (redundant) → async/await (concise and maintainable)

7. Comparison Table of the Four Read Approaches

Approach Blocks Thread Advantages Disadvantages
readFileSync Yes Extremely simple syntax Heavy I/O severely degrades service performance
readFile callback No No extra imports required Serial multi-file reads produce callback hell
Promise then No No nested structure Repetitive and verbose chaining for multiple files
async await No Linear reading, clean code ES8 syntax, requires wrapping in an async function

8. Engineering Best Practices

  1. For business file reads and writes, uniformly use fs/promises + async/await;
  2. All file reads must be paired with try/catch to capture exceptions;
  3. Synchronous readFileSync should only be used for loading configuration during project initialization; it is forbidden in endpoint business logic;
  4. Pair paths with path.resolve to generate absolute paths, preventing file-not-found errors caused by changes in the working directory.