Explain callback functions in JavaScript
The Short Answer
A callback function is a function passed as an argument to another function, which then calls ("calls back") that function at some point — either synchronously (immediately) or asynchronously (later, when some operation completes). Callbacks are JavaScript's original mechanism for handling async operations and for making functions customizable. They're everywhere: event handlers, array methods, timers, and Node.js APIs all use callbacks.
Synchronous Callbacks
Synchronous callbacks execute immediately within the function that receives them. Array methods like map, filter, and forEach are the most common examples — you pass a function that gets called once for each element, right there in the current execution flow.
// Array methods — callback runs immediately for each element
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((num) => num * 2);
// Callback (num) => num * 2 is called 5 times, synchronously
const evens = numbers.filter((num) => num % 2 === 0);
// Callback runs immediately, returns [2, 4]
// Custom function that accepts a callback
function processItems(items: string[], transform: (item: string) => string): string[] {
const results: string[] = [];
for (const item of items) {
results.push(transform(item)); // Call the callback
}
return results;
}
const uppercased = processItems(['hello', 'world'], (item) => item.toUpperCase());
// ['HELLO', 'WORLD']
With synchronous callbacks, the code after the function call doesn't run until all callbacks have completed. The execution is predictable and linear — no timing surprises.
Asynchronous Callbacks
Asynchronous callbacks are scheduled to run later — after a timer fires, a network request completes, a file is read, or an event occurs. The function that receives the callback returns immediately, and the callback runs at some future point when the async operation finishes.
// Timer — callback runs after 1 second
setTimeout(() => {
console.log('This runs later');
}, 1000);
console.log('This runs first');
// Event listener — callback runs when user clicks
button.addEventListener('click', () => {
console.log('Button was clicked');
});
// Node.js file read — callback runs when file is loaded
import { readFile } from 'fs';
readFile('/path/to/file.txt', 'utf8', (error, data) => {
if (error) {
console.error('Failed to read file:', error);
return;
}
console.log('File contents:', data);
});
// XMLHttpRequest (old-school AJAX)
const xhr = new XMLHttpRequest();
xhr.onload = () => console.log(xhr.responseText);
xhr.open('GET', '/api/data');
xhr.send();
The key difference from synchronous callbacks: code after the function call runs before the callback does. The callback is queued and executed by the event loop when the async operation completes.
The Error-First Pattern
Node.js established a convention for async callbacks: the first parameter is always the error (or null if successful), and subsequent parameters are the result data. This pattern ensures errors are always handled and provides a consistent API across all async operations.
// Error-first callback pattern
type Callback<T> = (error: Error | null, result?: T) => void;
function fetchUserData(userId: string, callback: Callback<User>) {
// Simulate async operation
setTimeout(() => {
if (!userId) {
callback(new Error('User ID is required'));
return;
}
callback(null, { id: userId, name: 'Alice' });
}, 100);
}
// Usage — always check error first
fetchUserData('123', (error, user) => {
if (error) {
console.error('Failed:', error.message);
return;
}
console.log('User:', user?.name);
});
Callbacks as Customization Points
Beyond async operations, callbacks make functions flexible and reusable. Instead of hardcoding behavior, you accept a callback that lets the caller decide what happens. This is the strategy pattern in its simplest form — the callback is the strategy.
// Sorting with a custom comparator callback
const users = [
{ name: 'Charlie', age: 30 },
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 35 },
];
// The callback defines HOW to sort
users.sort((a, b) => a.age - b.age); // Sort by age
users.sort((a, b) => a.name.localeCompare(b.name)); // Sort by name
// Retry logic with a callback for the operation
function retry<T>(operation: () => Promise<T>, maxAttempts: number): Promise<T> {
return operation().catch((error) => {
if (maxAttempts <= 1) throw error;
return retry(operation, maxAttempts - 1);
});
}
retry(() => fetch('/api/data'), 3);
Callbacks vs Promises vs Async/Await
Callbacks were JavaScript's only async mechanism for years, but they have ergonomic problems — especially when you need to chain multiple async operations. Promises and async/await were introduced to solve these issues while still using callbacks under the hood.
| Callbacks | Promises | Async/Await | |
|---|---|---|---|
| Error handling | Manual (check error param) | .catch() chain | try/catch block |
| Chaining | Nested callbacks (pyramid) | .then().then() | Sequential await lines |
| Readability | Poor when nested | Better (flat chain) | Best (looks synchronous) |
| Cancellation | Not built-in | Not built-in (use AbortController) | Not built-in |
| Parallel ops | Manual coordination | Promise.all() | Promise.all() with await |
Why Interviewers Ask This
Callbacks are foundational to JavaScript — understanding them is prerequisite to understanding promises, async/await, event handling, and functional programming patterns. Interviewers ask this to confirm you know the difference between sync and async callbacks, understand the error-first convention, can explain why callbacks lead to nesting problems, and know how modern alternatives (promises, async/await) improve on them. It's a building-block question that reveals your depth of JavaScript knowledge.
Quick Revision Cheat Sheet
Definition: A function passed to another function to be called later
Sync callbacks: Run immediately — array methods (map, filter, forEach)
Async callbacks: Run later — timers, events, I/O operations
Error-first: callback(error, result) — Node.js convention
Problem: Nested callbacks = callback hell (pyramid of doom)
Modern alternatives: Promises (.then/.catch) and async/await