Node.js Promises

Promises give you a clean, chainable way to work with asynchronous operations in Node.js, replacing deeply nested callbacks with readable, composable code.

What Is a Promise?

A Promise is an object representing the eventual completion or failure of an asynchronous operation. It exists in one of three states: pending (the initial state, neither fulfilled nor rejected), fulfilled (the operation completed successfully), or rejected (the operation failed). Once a promise settles into fulfilled or rejected, it stays that way forever — this is called being 'immutable' after settlement.

Node.js exposes promise-based versions of many core APIs. The most commonly used is the fs/promises module, which mirrors the callback-based fs module but returns promises instead of accepting callback arguments.

Reading Files with fs/promises

Instead of passing a callback that Node invokes later, fs/promises methods return a Promise you can attach handlers to with .then() and .catch(). This removes one level of nesting and makes error handling more consistent.

Reading a file with fs/promises

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

fs.readFile('notes.txt', 'utf8')
  .then((data) => {
    console.log('File contents:', data);
  })
  .catch((err) => {
    console.error('Failed to read file:', err.message);
  });

console.log('Read request sent, continuing...');
Note: The 'Read request sent...' line logs before the file contents because readFile is non-blocking. The promise resolves later, once the event loop picks up the completed I/O.

Chaining with .then() and .catch()

Each call to .then() returns a new promise, which is what makes chaining possible. You can transform a value in one .then(), pass it to the next, and catch any error that occurred anywhere in the chain with a single .catch() at the end.

Chaining multiple promise steps

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

fs.readFile('config.json', 'utf8')
  .then((raw) => JSON.parse(raw))
  .then((config) => {
    console.log('Loaded config for env:', config.environment);
    return fs.writeFile('config.log', `Loaded at ${new Date().toISOString()}`);
  })
  .then(() => {
    console.log('Log entry written.');
  })
  .catch((err) => {
    // Catches errors from readFile, JSON.parse, OR writeFile
    console.error('Pipeline failed:', err.message);
  });
Note: A single .catch() at the end of a chain catches errors from every preceding step. If you need different handling per step, catch locally inside a .then() instead of letting it bubble all the way down.

Running Promises in Parallel with Promise.all

When you have several independent asynchronous operations, awaiting them one after another wastes time. Promise.all() takes an array of promises and returns a single promise that resolves when all of them resolve, with an array of their results in the same order. If any promise rejects, Promise.all() rejects immediately with that reason.

MethodBehavior on rejectionResult shape
Promise.allRejects immediately if any promise rejectsArray of resolved values
Promise.allSettledNever rejects; reports status per promiseArray of {status, value|reason}
Promise.raceSettles as soon as the first promise settlesSingle value or reason

Reading multiple files concurrently

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

const files = ['a.txt', 'b.txt', 'c.txt'];

Promise.all(files.map((name) => fs.readFile(name, 'utf8')))
  .then((contents) => {
    contents.forEach((text, i) => {
      console.log(`${files[i]}: ${text.length} characters`);
    });
  })
  .catch((err) => {
    console.error('At least one file failed to read:', err.message);
  });
Note: If one file in the list might legitimately be missing and you don't want the whole batch to fail, reach for Promise.allSettled() instead — it always resolves, and you inspect each result's status individually.

Creating Your Own Promises

You can wrap any callback-based or event-based API in a promise using the Promise constructor, which takes an executor function with resolve and reject parameters.

Wrapping setTimeout in a promise

function wait(ms) {
  return new Promise((resolve, reject) => {
    if (ms < 0) {
      reject(new Error('Duration must be non-negative'));
      return;
    }
    setTimeout(() => resolve(`Waited ${ms}ms`), ms);
  });
}

wait(500).then(console.log).catch(console.error);

Promises are the foundation for async/await, covered next — every async function is really just syntax sugar over the promise mechanics you've seen here.

Exercise: Node.js Promises

What are the three possible states of a Promise?