Master the foundation of asynchronous programming in JavaScript. Learn how callbacks work, when to use them, and how to avoid common pitfalls.
A callback is a function passed as an argument to another function, executed after an operation completes. It's called a "callback" because it gets "called back" at a later time.
Callbacks are the foundation of asynchronous programming in JavaScript, enabling non-blocking operations.
function greet(name, callback) {
console.log('Hello ' + name);
callback();
}
function sayGoodbye() {
console.log('Goodbye!');
}
// Execute
greet('John', sayGoodbye);
JavaScript doesn't wait for long operations to complete before moving to the next line of code.
Handle data fetching without freezing the UI
Read/write files efficiently in Node.js
Schedule code execution and handle user interactions
console.log('Start');
setTimeout(() => {
console.log('This runs after 2 seconds');
}, 2000);
console.log('End');
Understanding the difference between synchronous and asynchronous execution
Executed immediately within the function they're passed to. Common in array methods.
const numbers = [1, 2, 3, 4, 5];
// forEach
numbers.forEach((num) => {
console.log(num * 2);
});
// map
const doubled = numbers.map((num) => num * 2);
// filter
const evens = numbers.filter((num) => num % 2 === 0);
Executed later, after an asynchronous operation completes. Used for timers, APIs, and I/O.
console.log('Start');
setTimeout(() => {
console.log('Callback after 2s');
}, 2000);
console.log('End');
// Simulating API call
function fetchUserData(userId, callback) {
setTimeout(() => {
const user = { id: userId, name: 'John' };
callback(user);
}, 1000);
}
Practical implementations you'll encounter in production code
Handle user interactions by passing callback functions to event listeners.
// Traditional function
button.addEventListener('click', function() {
console.log('Button clicked!');
});
// Arrow function (modern)
button.addEventListener('click', () => {
console.log('Clicked with arrow!');
});
Node.js standard: first parameter is always an error object (if any), second is the data.
function divideNumbers(a, b, callback) {
if (b === 0) {
callback(new Error('Cannot divide by zero'), null);
} else {
callback(null, a / b);
}
}
divideNumbers(10, 2, (err, result) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('Result:', result);
});
Sequential operations using nested callbacks. This pattern can lead to deeply nested code.
step1(() => {
step2(() => {
step3(() => {
console.log('All steps complete!');
});
});
});
Best practices to write clean, maintainable callback-based code
Deeply nested callbacks create unreadable, hard-to-maintain code.
getData((data1) => {
processData(data1, (data2) => {
saveData(data2, (data3) => {
sendEmail(data3, (result) => {
// Too deep!
});
});
});
});
Break into smaller, named functions for better readability.
function handleData1(data1) {
processData(data1, handleData2);
}
function handleData2(data2) {
saveData(data2, handleData3);
}
getData(handleData1);
Chain async operations with .then() and .catch()
Syntactic sugar over Promises for cleaner code
Break code into smaller, reusable functions
Callbacks are functions passed as arguments to other functions
Executed later after some operation completes
Can be synchronous (array methods) or asynchronous (timers, APIs)
Error-first convention is standard in Node.js environment
💡 Pro Tip: For complex async flows, consider using Promises or Async/Await to avoid callback hell and write more maintainable code.