Skip to main content

Command Palette

Search for a command to run...

Async Code in Node.js: Callbacks and Promises

Updated
•2 min read•View as Markdown
Async Code in Node.js: Callbacks and Promises

Node.js relies on asynchronous programming to handle I/O operations efficiently without blocking the main thread. This blog explores why async code matters, using file reading examples to compare callbacks and promises.

Why Async Exists

Node.js runs on a single-threaded event loop, making non-blocking I/O essential for performance. Synchronous operations like fs.readFileSync() halt execution until complete, which works for simple scripts but fails under load—imagine a web server freezing while reading files. Async code lets the event loop process other tasks, like handling HTTP requests, while waiting for disk or network operations.

Callback Example

Start with a basic file read using callbacks from the fs module.

const fs = require('node:fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Error:', err);
    return;
  }
  console.log(data);
});

Step-by-step flow:

  1. readFile initiates the async operation and immediately returns control to the event loop.

  2. The OS completes the read; Node.js queues the callback.

  3. Event loop executes the callback, checking err first (error-first convention).nodejs+1

  4. If no error, data holds the file content.

This non-blocking approach shines for servers but gets messy with multiple operations.

Nested Callback Problems

Chaining callbacks creates "callback hell"—deeply nested code that's hard to read and maintain.

fs.readFile('file1.txt', 'utf8', (err1, data1) => {
  if (err1) return console.error(err1);
  fs.readFile('file2.txt', 'utf8', (err2, data2) => {
    if (err2) return console.error(err2);
    fs.writeFile('output.txt', data1 + data2, (err3) => {
      if (err3) return console.error(err3);
      console.log('Done!');
    });
  });
});

Error handling repeats, indentation grows, and debugging becomes painful.

Promise-Based Handling

Promises replace nested callbacks with chaining. Use Node's fs.promises for built-in support.

const fs = require('node:fs/promises');

async function processFiles() {
  try {
    const data1 = await fs.readFile('file1.txt', 'utf8');
    const data2 = await fs.readFile('file2.txt', 'utf8');
    await fs.writeFile('output.txt', data1 + data2);
    console.log('Done!');
  } catch (err) {
    console.error('Error:', err);
  }
}

processFiles();

Promises represent eventual completion: pending, fulfilled (.then()), or rejected (.catch()).