Callbacks in JavaScript
January 24, 2026
Callbacks in JavaScript
Overview
In JavaScript, a callback is a function passed into another function as an argument to be executed later. Callbacks are fundamental to JavaScript's asynchronous programming model and are used extensively in event handling, timers, and asynchronous operations.
Understanding Asynchronous Programming
JavaScript is a synchronous single-threaded language, but with the help of event-loop and promises, JavaScript is used to do asynchronous programming.
- Synchronous: Code runs in a particular sequence of instructions given in the program
- Asynchronous: Code execution allows the execution of upcoming instructions immediately, avoiding blocking of tasks due to previous instructions
Note: Asynchronous is a non-blocking architecture, so the execution of one task isn't dependent on another. Tasks can run simultaneously. Synchronous is a blocking architecture, so the execution of each operation is dependent on the completion of the one before it.
Basic Callback Example
function calculateMultiply(sum) {
return sum * 2;
}
function sum(x, y, callback) {
let s = x + y;
return callback(s); // calculateMultiply is being used as callback
}
const result = sum(2, 5, calculateMultiply);
console.log(result); // 14
Common Callback Patterns
1. setTimeout Callback
setTimeout(() => {
console.log("This runs after 1 second");
}, 1000);
2. Array Methods
const numbers = [1, 2, 3, 4, 5];
numbers.forEach((num) => {
console.log(num);
});
const doubled = numbers.map((num) => num * 2);
3. Event Handlers
button.addEventListener('click', (event) => {
console.log('Button clicked!');
});
Callback Hell
When callbacks are nested deeply, it creates "callback hell" or "pyramid of doom":
function sum(s, callback) {
callback(s + 1);
}
function multiply(s, callback) {
callback(s * 10);
}
function subtract(s, callback) {
callback(s - 10);
}
function divide(s) {
console.log(s / 2);
return s / 2;
}
// Callback hell - nested callbacks
const result = sum(10, (action) => {
console.log(action);
multiply(action, (action2) => {
console.log(action2);
subtract(action2, (action3) => {
console.log(action3);
divide(action3);
});
});
});
Problems with Callbacks
- Callback Hell: Deeply nested callbacks become hard to read and maintain
- Error Handling: Difficult to handle errors in nested callbacks
- Inversion of Control: You lose control over when and how callbacks execute
- Hard to Debug: Stack traces can be confusing with nested callbacks
Modern Alternatives
Promises
sum(10)
.then(multiply)
.then(subtract)
.then(divide)
.catch(handleError);
Async/Await
async function processData() {
try {
const result1 = await sum(10);
const result2 = await multiply(result1);
const result3 = await subtract(result2);
const final = await divide(result3);
return final;
} catch (error) {
handleError(error);
}
}
When to Use Callbacks
Callbacks are still useful for:
- Simple event handlers
- Array iteration methods (forEach, map, filter)
- Simple asynchronous operations
- Library APIs that require callbacks
- Node.js style APIs
Best Practices
- Keep callbacks simple: Avoid complex logic in callbacks
- Handle errors: Always include error handling in callbacks
- Use named functions: Instead of anonymous functions for better debugging
- Consider Promises/Async-Await: For complex asynchronous flows
- Avoid deep nesting: Refactor nested callbacks into separate functions
Example: Named Callback Function
function handleSuccess(data) {
console.log('Success:', data);
}
function handleError(error) {
console.error('Error:', error);
}
fetchData(url, handleSuccess, handleError);