Callbacks
Before promises, Node used callbacks for everything — and many libraries and older codebases still do. You need to read them, use them correctly, and convert them to promises.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Read and write error-first callbacks
- Recognise callback hell
- Flatten callbacks with named functions
- Convert callback APIs with util.promisify
Explanation#
A callback is a function you pass to another function, to be called when the work finishes.
Node’s convention is error-first: callback(err, result). If err is not null, something failed and you must handle it before using result.
Nesting callbacks for sequential steps creates “callback hell”: deep indentation, repeated error checks and code that is hard to follow.
util.promisify(fn) turns an error-first callback function into one that returns a promise, so you can use await. Most core modules also have promise versions (fs/promises, dns/promises, timers/promises).
Callbacks are still fine for events and simple hooks (array.map, emitter.on) — the problem is only deeply nested async steps.
Syntax#
fs.readFile('a.txt', 'utf8', (err, data) => {
if (err) return handle(err);
use(data);
});
const readFileAsync = util.promisify(fs.readFile);
const data = await readFileAsync('a.txt', 'utf8');
Code example: From callback hell to async/await#
import { promisify } from 'node:util';
// A callback-style API (like many older libraries)
const db = {
findUser(id, cb) {
setTimeout(() => (id === 7 ? cb(null, { id, name: 'Asha' }) : cb(new Error(`User ${id} not found`))), 20);
},
findOrders(userId, cb) {
setTimeout(() => cb(null, [{ id: 1001, total: 499 }, { id: 1002, total: 60 }]), 20);
},
};
// 1. Nested callbacks ("callback hell")
db.findUser(7, (err, user) => {
if (err) return console.log('Error:', err.message);
db.findOrders(user.id, (err2, orders) => {
if (err2) return console.log('Error:', err2.message);
console.log(`[callbacks] ${user.name}: ${orders.length} orders`);
});
});
// 2. The same with promisify + async/await
const findUser = promisify(db.findUser);
const findOrders = promisify(db.findOrders);
async function summary(id) {
try {
const user = await findUser(id);
const orders = await findOrders(user.id);
const total = orders.reduce((s, o) => s + o.total, 0);
return `[async/await] ${user.name}: ${orders.length} orders, ₹${total}`;
} catch (err) {
return `[async/await] Error: ${err.message}`;
}
}
console.log(await summary(7));
console.log(await summary(99));
[callbacks] Asha: 2 orders [async/await] Asha: 2 orders, ₹559 [async/await] Error: User 99 not found
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- db methods follow the error-first convention: cb(err) or cb(null, result).
- The nested version repeats error checks and grows to the right with every step.
- promisify converts each method into a promise-returning function.
- summary reads top to bottom, with one try/catch for all errors.
- An unknown user rejects the promise and is handled in catch.
Real-world use case#
A team maintaining a 2015 Express app wraps the old callback-based payment SDK with util.promisify, so all new route handlers can use async/await without rewriting the SDK.
Common mistakes#
Using result without checking err leads to “cannot read property of undefined” later, far from the real cause.
Forgetting return after cb(err) continues and calls cb again. Always return when you call the callback for an error.
A throw inside a setTimeout or fs callback is not caught by an outer try/catch. Pass errors to the callback (or use promises).
Best practices#
- Use promise APIs for new code
- Wrap legacy callback APIs with util.promisify
- Always check err first
- Return after calling an error callback
Remember this#
Error-first: callback(err, result).
Deep nesting = callback hell → promisify + await.
return cb(err) — never call a callback twice.
Practice exercise#
Your task: Promisify this function and await it: function add(a, b, cb) { setTimeout(() => cb(null, a + b), 10); }
Need a hint?
Show solution
import { promisify } from 'node:util';
function add(a, b, cb) {
setTimeout(() => cb(null, a + b), 10);
}
const addAsync = promisify(add);
console.log(await addAsync(2, 3)); // 5
Mini quiz#
In an error-first callback, what is the first argument?
Want more? Practise all Node.js MCQs · Node.js interview questions
My notes#
Saved in this browser.