Skip to content
Plurafy

Callbacks and Promise Chaining

Understanding asynchronous callback patterns

Avoid callback hell, properly chain asynchronous operations, and handle errors across chains.

Callbacks are the foundation of asynchronous JavaScript. Modern code prefers Promises and async/await, but callbacks remain important.

// Synchronous callback
function forEach(array, callback) {
for (let i = 0; i < array.length; i++) {
callback(array[i], i);
}
}
forEach([1, 2, 3], (item, index) => {
console.log(`${index}: ${item}`);
});
// Asynchronous callback
function fetchUser(id, callback) {
setTimeout(() => {
const user = { id, name: 'John' };
callback(user);
}, 1000);
}
fetchUser(1, (user) => {
console.log('User:', user);
});
// ❌ Callback hell (pyramid of doom)
fetchUser(1, (user) => {
fetchPosts(user.id, (posts) => {
fetchComments(posts[0].id, (comments) => {
displayComments(comments);
});
});
});
// ✅ Named functions
function handleUser(user) {
fetchPosts(user.id, handlePosts);
}
function handlePosts(posts) {
fetchComments(posts[0].id, handleComments);
}
function handleComments(comments) {
displayComments(comments);
}
fetchUser(1, handleUser);
// ✅ Promises (better)
fetchUserPromise(1)
.then(user => fetchPostsPromise(user.id))
.then(posts => fetchCommentsPromise(posts[0].id))
.then(comments => displayComments(comments));
// Error-first callback convention
function readFile(path, callback) {
fs.readFile(path, (error, data) => {
if (error) {
callback(error); // Pass error as first arg
} else {
callback(null, data); // null for no error
}
});
}
readFile('file.txt', (error, data) => {
if (error) {
console.error('Failed to read:', error);
} else {
console.log('File content:', data);
}
});
// Multiple operations with error handling
function processFile(path, callback) {
readFile(path, (error, data) => {
if (error) return callback(error);
parseData(data, (error, parsed) => {
if (error) return callback(error);
saveData(parsed, (error) => {
if (error) return callback(error);
callback(null, 'Success');
});
});
});
}
// Convert callback to Promise
function promisify(fn) {
return (...args) => {
return new Promise((resolve, reject) => {
fn(...args, (error, result) => {
if (error) reject(error);
else resolve(result);
});
});
};
}
const readFileAsync = promisify(fs.readFile);
// Now use as Promise
readFileAsync('file.txt', 'utf8')
.then(data => console.log(data))
.catch(error => console.error(error));
// Built-in promisify (Node.js)
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
// Queue callbacks and run sequentially
class Queue {
constructor() {
this.callbacks = [];
}
add(callback) {
this.callbacks.push(callback);
return this;
}
run() {
if (this.callbacks.length === 0) return;
const callback = this.callbacks.shift();
callback(() => this.run());
}
}
const queue = new Queue();
queue
.add((next) => {
setTimeout(() => {
console.log('Task 1');
next();
}, 1000);
})
.add((next) => {
console.log('Task 2');
next();
})
.run();
// Observer pattern with callbacks
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(cb => cb(data));
}
}
}
const emitter = new EventEmitter();
emitter.on('data', (value) => {
console.log('Received:', value);
});
emitter.emit('data', 'hello');
// Listener returning async result
class AsyncQueue {
constructor() {
this.listeners = [];
}
subscribe(listener) {
this.listeners.push(listener);
}
async process(task) {
for (const listener of this.listeners) {
await listener(task);
}
}
}
// ✅ Good uses for callbacks
// 1. Event listeners
element.addEventListener('click', () => {});
// 2. Array methods
[1, 2, 3].map(n => n * 2);
// 3. Simple error-first pattern
fs.readFile('file.txt', (error, data) => {});
// ❌ Avoid callbacks for:
// 1. Complex chains (use Promises/async-await)
// 2. Error propagation across multiple levels
// 3. Multiple concurrent operations (use Promise.all)