A callback is a function passed as an argument into another function, which is then invoked inside the outer function to complete some kind of action. Callbacks are the foundation of asynchronous programming in JavaScript and one of the most important patterns to master.
At its simplest, a callback is just a function that you pass to another function to be executed later.
function greet(name, callback) {
console.log("Hello, " + name);
callback();
}
function sayGoodbye() {
console.log("Goodbye!");
}
greet("Alice", sayGoodbye);
// Output:
// Hello, Alice
// Goodbye!The function sayGoodbye is passed as a callback to greet and is executed after the greeting.
These callbacks execute immediately, blocking further execution until complete.
const numbers = [1, 2, 3, 4, 5];
// forEach with callback
numbers.forEach(function(num) {
console.log(num * 2);
});
// map with callback
const doubled = numbers.map(num => num * 2);
// filter with callback
const evens = numbers.filter(num => num % 2 === 0);
// reduce with callback
const sum = numbers.reduce((acc, num) => acc + num, 0);function processData(data, processor) {
const results = [];
for (const item of data) {
results.push(processor(item));
}
return results;
}
const numbers = [1, 2, 3];
const squared = processData(numbers, n => n * n);
console.log(squared); // [1, 4, 9]These callbacks execute after some asynchronous operation completes — a network request finishes, a timer expires, or a user clicks a button.
console.log("Start");
setTimeout(function() {
console.log("This runs after 2 seconds");
}, 2000);
console.log("End");
// Output:
// Start
// End
// This runs after 2 seconds (after 2 seconds)document.getElementById("myButton").addEventListener("click", function() {
console.log("Button clicked!");
});const fs = require("fs");
fs.readFile("file.txt", "utf8", function(error, data) {
if (error) {
console.error("Error reading file:", error);
return;
}
console.log("File contents:", data);
});
console.log("Reading file...");A standard callback takes an error as the first argument and the result as the second:
function fetchData(callback) {
// Simulate async operation
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
callback(null, { data: "Some data" });
} else {
callback(new Error("Failed to fetch data"), null);
}
}, 1000);
}
// Using the callback
fetchData(function(error, result) {
if (error) {
console.error("Error:", error.message);
return;
}
console.log("Success:", result);
});This error-first callback pattern (also called Node.js style) is the standard convention.
When you have multiple nested asynchronous operations, callbacks can become deeply nested and hard to read:
getUserData(userId, function(error, user) {
if (error) {
console.error(error);
return;
}
getOrders(user.id, function(error, orders) {
if (error) {
console.error(error);
return;
}
getProducts(orders[0].id, function(error, products) {
if (error) {
console.error(error);
return;
}
getReviews(products[0].id, function(error, reviews) {
if (error) {
console.error(error);
return;
}
console.log(reviews);
});
});
});
});- Hard to read — deep indentation makes code difficult to follow
- Error handling duplication — every level needs error checking
- Difficult to maintain — adding or removing steps is painful
- No linear flow — execution order doesn't match code order
function handleReviews(error, reviews) {
if (error) {
console.error(error);
return;
}
console.log(reviews);
}
function handleProducts(error, products) {
if (error) {
console.error(error);
return;
}
getReviews(products[0].id, handleReviews);
}
function handleOrders(error, orders) {
if (error) {
console.error(error);
return;
}
getProducts(orders[0].id, handleProducts);
}
function handleUser(error, user) {
if (error) {
console.error(error);
return;
}
getOrders(user.id, handleOrders);
}
getUserData(userId, handleUser);This is cleaner but spreads related code across many functions.
Promises provide a cleaner way to chain asynchronous operations:
getUserData(userId)
.then(user => getOrders(user.id))
.then(orders => getProducts(orders[0].id))
.then(products => getReviews(products[0].id))
.then(reviews => console.log(reviews))
.catch(error => console.error(error));async function fetchReviewData(userId) {
try {
const user = await getUserData(userId);
const orders = await getOrders(user.id);
const products = await getProducts(orders[0].id);
const reviews = await getReviews(products[0].id);
console.log(reviews);
} catch (error) {
console.error(error);
}
}We'll cover Promises and Async/Await in detail in later sections. For now, understand that callbacks are the underlying mechanism.
Functions that accept callbacks are a form of higher-order function:
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, console.log);
// 0
// 1
// 2
repeat(3, i => {
console.log(`Iteration ${i + 1}`);
});
// Iteration 1
// Iteration 2
// Iteration 3const fruits = ["apple", "banana", "cherry"];
fruits.forEach(function(fruit, index) {
console.log(`${index}: ${fruit}`);
});const users = [
{ firstName: "Alice", lastName: "Smith" },
{ firstName: "Bob", lastName: "Jones" }
];
const fullNames = users.map(function(user) {
return `${user.firstName} ${user.lastName}`;
});
console.log(fullNames); // ["Alice Smith", "Bob Jones"]function myFilter(array, callback) {
const results = [];
for (let i = 0; i < array.length; i++) {
if (callback(array[i], i, array)) {
results.push(array[i]);
}
}
return results;
}
const numbers = [1, 2, 3, 4, 5, 6];
const evens = myFilter(numbers, num => num % 2 === 0);
console.log(evens); // [2, 4, 6]Be careful with this inside callbacks:
const user = {
name: "Alice",
friends: ["Bob", "Charlie"],
showFriends: function() {
this.friends.forEach(function(friend) {
console.log(`${this.name} is friends with ${friend}`);
// ❌ this.name is undefined!
});
}
};
// Fix 1: Arrow function (inherits this)
showFriends: function() {
this.friends.forEach(friend => {
console.log(`${this.name} is friends with ${friend}`);
});
}
// Fix 2: Store this in a variable
showFriends: function() {
const self = this;
this.friends.forEach(function(friend) {
console.log(`${self.name} is friends with ${friend}`);
});
}
// Fix 3: bind()
showFriends: function() {
this.friends.forEach(function(friend) {
console.log(`${this.name} is friends with ${friend}`);
}.bind(this));
}function delay(ms, callback) {
setTimeout(() => {
callback();
}, ms);
}
delay(1000, () => {
console.log("1 second passed");
});function readJsonFile(filePath, callback) {
fs.readFile(filePath, "utf8", (error, data) => {
if (error) {
callback(error);
return;
}
try {
const json = JSON.parse(data);
callback(null, json);
} catch (parseError) {
callback(parseError);
}
});
}
readJsonFile("data.json", (error, data) => {
if (error) {
console.error("Failed:", error.message);
return;
}
console.log("Data:", data);
});// ❌ Wrong — calls the function immediately
setTimeout(sayHello(), 1000);
// ✅ Right — passes the function reference
setTimeout(sayHello, 1000);
// ✅ Also right — arrow function wrapping
setTimeout(() => sayHello(), 1000);// ❌ Missing error check
fs.readFile("file.txt", (error, data) => {
console.log(data); // Could be undefined!
});
// ✅ Always check errors first
fs.readFile("file.txt", (error, data) => {
if (error) {
console.error(error);
return;
}
console.log(data);
});// ❌ Confusing — looks async but isn't
function maybeAsync(callback) {
if (cache.hasData) {
callback(cache.data); // Synchronous!
} else {
fetchData(callback); // Asynchronous
}
}
// ✅ Always async (use setTimeout or process.nextTick)
function alwaysAsync(callback) {
if (cache.hasData) {
setTimeout(() => callback(null, cache.data), 0);
} else {
fetchData(callback);
}
}// ❌ Callback called twice!
function process(data, callback) {
if (data.valid) {
callback(null, data);
}
callback(new Error("Invalid")); // Always called!
}
// ✅ Return after calling callback
function process(data, callback) {
if (data.valid) {
return callback(null, data);
}
callback(new Error("Invalid"));
}Implement your own forEach that accepts a callback.
function myForEach(array, callback) {
// Your code
}
myForEach([1, 2, 3], (item, index) => {
console.log(`${index}: ${item}`);
});Write a function that runs an array of async operations sequentially using callbacks.
function runSequential(tasks, finalCallback) {
// tasks is an array of functions that take a callback
}
const tasks = [
cb => setTimeout(() => { console.log("Task 1"); cb(); }, 100),
cb => setTimeout(() => { console.log("Task 2"); cb(); }, 50),
cb => setTimeout(() => { console.log("Task 3"); cb(); }, 200)
];
runSequential(tasks, () => console.log("All done!"));Create a function that retries an async operation up to N times.
function retry(fn, maxRetries, callback) {
// fn is a function that takes (error, result) callback
}Write a function that runs async operations in parallel and collects all results.
function runParallel(tasks, finalCallback) {
// Run all tasks simultaneously
// Call finalCallback(error, results) when all complete
}Wrap a callback-based function with a timeout.
function withTimeout(fn, ms) {
// Return a new function that fails if fn doesn't call back within ms
}- A callback is a function passed as an argument to another function
- Synchronous callbacks execute immediately (array methods)
- Asynchronous callbacks execute later (timers, events, I/O)
- Use the error-first pattern:
callback(error, result) - Callback hell occurs with deeply nested async operations
- Solutions: named functions, Promises, or async/await
- Be careful with
thiscontext inside callbacks - Always check for errors before using results
- Call callbacks exactly once and prefer always-async behavior
Callbacks lead naturally into:
- Promises — cleaner async handling
- Async/Await — synchronous-looking async code
- Event Loop — how JavaScript handles async operations
Happy coding! 🚀