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...');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);
});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.
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);
});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?