Callbacks are functions passed to other functions to be executed later. They’re fundamental to asynchronous programming in JavaScript.
What is Asynchronous Programming?
Asynchronous code doesn’t execute line-by-line. It allows operations to run in the background:
// Synchronous - blocks execution
console.log("Start");
const result = heavyComputation(); // Waits for completion
console.log("End");
// Asynchronous - doesn't block
console.log("Start");
heavyComputationAsync(() => {
console.log("Done"); // Runs later
});
console.log("End"); // Runs immediately
Callbacks
A callback is a function passed as an argument to another function:
function greet(name, callback) {
console.log(`Hello, ${name}!`);
callback();
}
function sayGoodbye() {
console.log("Goodbye!");
}
greet("Alice", sayGoodbye);
// Output:
// Hello, Alice!
// Goodbye!
Callbacks with Parameters
function fetchUser(userId, callback) {
// Simulate API call
setTimeout(() => {
const user = { id: userId, name: "Alice" };
callback(user);
}, 1000);
}
fetchUser(1, (user) => {
console.log(user); // { id: 1, name: "Alice" }
});
Error Handling with Callbacks
function fetchData(url, onSuccess, onError) {
setTimeout(() => {
if (url) {
onSuccess({ data: "Success" });
} else {
onError("URL is required");
}
}, 1000);
}
fetchData(
"https://api.example.com",
(data) => console.log(data),
(error) => console.error(error)
);
Node.js Error-First Callbacks
function readFile(filename, callback) {
// Error-first convention: (error, data)
setTimeout(() => {
if (filename) {
callback(null, "File contents");
} else {
callback(new Error("Filename required"));
}
}, 1000);
}
readFile("file.txt", (error, data) => {
if (error) {
console.error(error);
} else {
console.log(data);
}
});
Callback Hell (Pyramid of Doom)
Multiple nested callbacks become hard to read:
// Callback Hell
getUser(userId, (error, user) => {
if (error) {
console.error(error);
} else {
getOrders(user.id, (error, orders) => {
if (error) {
console.error(error);
} else {
getOrderDetails(orders[0].id, (error, details) => {
if (error) {
console.error(error);
} else {
console.log(details);
}
});
}
});
}
});
Array Callbacks
Array methods use callbacks:
const numbers = [1, 2, 3, 4, 5];
// forEach
numbers.forEach((num) => {
console.log(num);
});
// map
const doubled = numbers.map((num) => num * 2);
// filter
const evens = numbers.filter((num) => num % 2 === 0);
// find
const first = numbers.find((num) => num > 3);
Event Callbacks
Event handlers are callbacks:
const button = document.getElementById("myButton");
button.addEventListener("click", (event) => {
console.log("Button clicked!");
});
button.addEventListener("mouseover", (event) => {
console.log("Mouse over button");
});
setTimeout and setInterval
Common asynchronous operations:
// setTimeout - execute once after delay
setTimeout(() => {
console.log("Executed after 1 second");
}, 1000);
// setInterval - execute repeatedly
const intervalId = setInterval(() => {
console.log("Executed every 1 second");
}, 1000);
// Clear interval
setTimeout(() => {
clearInterval(intervalId);
}, 5000);
Practical Examples
Retry Logic
function retryOperation(operation, maxRetries = 3, callback) {
let attempts = 0;
function attempt() {
attempts++;
operation((error, result) => {
if (error && attempts < maxRetries) {
console.log(`Attempt ${attempts} failed, retrying...`);
attempt();
} else {
callback(error, result);
}
});
}
attempt();
}
retryOperation(
(cb) => {
// Simulated operation
if (Math.random() > 0.7) {
cb(null, "Success");
} else {
cb(new Error("Failed"));
}
},
3,
(error, result) => {
if (error) {
console.error("All attempts failed");
} else {
console.log(result);
}
}
);
Waterfall Pattern
function waterfall(tasks, callback) {
let index = 0;
function next(error, result) {
if (error) {
return callback(error);
}
if (index >= tasks.length) {
return callback(null, result);
}
const task = tasks[index++];
task(result, next);
}
next(null);
}
waterfall([
(data, callback) => {
console.log("Step 1");
callback(null, "result1");
},
(data, callback) => {
console.log("Step 2:", data);
callback(null, "result2");
},
(data, callback) => {
console.log("Step 3:", data);
callback(null, "final result");
}
], (error, result) => {
console.log("Done:", result);
});
Parallel Execution
function parallel(tasks, callback) {
const results = [];
let completed = 0;
tasks.forEach((task, index) => {
task((error, result) => {
if (error) {
return callback(error);
}
results[index] = result;
completed++;
if (completed === tasks.length) {
callback(null, results);
}
});
});
}
parallel([
(cb) => setTimeout(() => cb(null, "result1"), 1000),
(cb) => setTimeout(() => cb(null, "result2"), 500),
(cb) => setTimeout(() => cb(null, "result3"), 1500)
], (error, results) => {
console.log(results); // ["result1", "result2", "result3"]
});
Best Practices
Use Named Functions
// Good - clear intent
function handleSuccess(data) {
console.log(data);
}
function handleError(error) {
console.error(error);
}
fetchData(url, handleSuccess, handleError);
// Avoid - anonymous functions
fetchData(url, (data) => console.log(data), (error) => console.error(error));
Keep Callbacks Simple
// Good - simple callback
array.forEach((item) => {
console.log(item);
});
// Avoid - complex logic in callback
array.forEach((item) => {
if (item.active) {
const processed = item.value * 2;
const formatted = processed.toFixed(2);
console.log(formatted);
}
});
Use Promises or Async/Await
// Avoid - callback hell
getUser(id, (error, user) => {
if (error) {
handleError(error);
} else {
getOrders(user.id, (error, orders) => {
if (error) {
handleError(error);
} else {
console.log(orders);
}
});
}
});
// Better - use promises
getUser(id)
.then(user => getOrders(user.id))
.then(orders => console.log(orders))
.catch(error => handleError(error));
// Best - use async/await
async function getOrdersForUser(id) {
try {
const user = await getUser(id);
const orders = await getOrders(user.id);
console.log(orders);
} catch (error) {
handleError(error);
}
}
Summary
- Callback: function passed to another function
- Asynchronous: code that doesn’t block execution
- Callback hell: deeply nested callbacks (avoid with Promises/async-await)
- Error-first: convention for Node.js callbacks
- Array methods: use callbacks for iteration
- Events: use callbacks for event handling
- Best practice: use Promises or async/await instead of callbacks
Related Resources
Official Documentation
Next Steps
- Promises: Creation, Chaining, Resolution
- Async/Await: Modern Asynchronous Programming
- Fetch API: Making HTTP Requests
Comments