Introduction
Before Promises, callbacks were the only way to handle async operations. While modern code prefers async/await, callbacks still appear everywhere—event handlers, array methods, and many Node.js APIs. Understanding them helps you work with any JavaScript codebase.
Key Concepts
Callback: A function passed to another function to be called later.
Error-First Callback: Node.js convention where first parameter is error.
Callback Hell: Deeply nested callbacks that are hard to read.
Real World Context
Event listeners, setTimeout, fs.readFile, Express middleware—callbacks are everywhere. Even if you prefer Promises, you'll encounter callback APIs regularly.
Deep Dive
Basic Callbacks
javascriptfunction fetchData(callback) { setTimeout(() => { callback({ id: 1, name: 'Alice' }); }, 1000); } fetchData((data) => { console.log('Got data:', data); });
Error-First Callbacks (Node.js Style)
javascriptfunction readFile(path, callback) { setTimeout(() => { if (!path) { callback(new Error('Path required'), null); return; } callback(null, 'file contents'); }, 100); } readFile('/data.txt', (error, data) => { if (error) { console.error('Failed:', error.message); return; } console.log('Data:', data); });
Callback Hell (The Problem)
javascript// Deeply nested, hard to follow getUser(userId, (err, user) => { if (err) return handleError(err); getOrders(user.id, (err, orders) => { if (err) return handleError(err); getOrderDetails(orders[0].id, (err, details) => { if (err) return handleError(err); getShippingInfo(details.shipId, (err, shipping) => { if (err) return handleError(err); displayOrder(user, orders[0], details, shipping); }); }); }); });
Escaping Callback Hell
javascript// 1. Named functions function handleUser(err, user) { if (err) return handleError(err); getOrders(user.id, handleOrders); } function handleOrders(err, orders) { if (err) return handleError(err); getOrderDetails(orders[0].id, handleDetails); } getUser(userId, handleUser); // 2. Promise wrapping (best solution) function promisify(fn) { return (...args) => new Promise((resolve, reject) => { fn(...args, (err, result) => { if (err) reject(err); else resolve(result); }); }); } const getUserPromise = promisify(getUser);
Common Callback Patterns
javascript// Array methods with callbacks [1, 2, 3].map(n => n * 2); // Transform [1, 2, 3].filter(n => n > 1); // Filter [1, 2, 3].forEach(n => log(n)); // Side effects // Event handlers button.addEventListener('click', (event) => { console.log('Clicked!', event.target); }); // setTimeout/setInterval const timerId = setTimeout(() => { console.log('Delayed execution'); }, 1000); clearTimeout(timerId); // Cancel
Common Pitfalls
- Forgetting to handle errors: Always check error parameter.
- Callback not called: Leads to hanging operations.
- Callback called multiple times: Causes bugs; use once wrappers.
Best Practices
- Wrap in Promises: Use
util.promisifyor custom wrapper. - Use named functions: Avoid deep nesting.
- Always handle errors: Check error-first parameter.
- Prefer async/await: When working with Promises.
Summary
Callbacks are functions passed to other functions for later execution. Node.js uses error-first convention. Callback hell is solved by named functions or promisification. Modern code prefers Promises/async-await, but callbacks remain common in events and many APIs.
Code Examples
function fetchData(callback) {
setTimeout(() => {
callback({ id: 1, name: 'Alice' });
}, 1000);
}
fetchData((data) => {
console.log('Got data:', data);
});function readFile(path, callback) {
setTimeout(() => {
if (!path) {
callback(new Error('Path required'), null);
return;
}
callback(null, 'file contents');
}, 100);
}
readFile('/data.txt', (error, data) => {
if (error) {
console.error('Failed:', error.message);
return;
}
console.log('Data:', data);
});