Docs
Module-15-Asynchronous-JavaScript
9.1 Callbacks
Introduction
A callback is a function passed as an argument to another function, to be executed later (after some operation completes). Callbacks are the foundation of asynchronous programming in JavaScript.
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā SYNCHRONOUS VS ASYNCHRONOUS ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā SYNCHRONOUS ASYNCHRONOUS ā
ā āāāāāāāāāāāā āāāāāāāāāāāā ā
ā Task 1 āāāāāāŗ Task 1 āāāāāāŗ ā
ā ā ā ā
ā Task 2 āāāāāāŗ Task 2 āāāāāāŗ (waiting) ā
ā ā āāāāāāāā ā
ā Task 3 āāāāāāŗ Task 3 āāāāāāŗ ā
ā ā
ā Blocking Non-blocking ā
ā (one at a time) (parallel work possible) ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
What is a Callback?
// A callback is just a function passed to another function
function greet(name, callback) {
console.log('Hello, ' + name);
callback();
}
function sayGoodbye() {
console.log('Goodbye!');
}
greet('Alice', sayGoodbye);
// Output:
// Hello, Alice
// Goodbye!
Synchronous Callbacks
Callbacks that execute immediately (not waiting for anything):
// Array methods use synchronous callbacks
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(function (num) {
console.log(num); // Executes immediately for each element
});
const doubled = numbers.map((n) => n * 2);
const filtered = numbers.filter((n) => n > 2);
Asynchronous Callbacks
Callbacks that execute later (after some operation):
// setTimeout - executes after delay
setTimeout(function () {
console.log('This runs after 2 seconds');
}, 2000);
console.log('This runs immediately');
// Output:
// This runs immediately
// This runs after 2 seconds
The Event Loop
Understanding how JavaScript handles async operations:
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā EVENT LOOP ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāāā ā
ā ā Call Stack ā ā Web APIs ā ā Task Queue ā ā
ā ā ā ā ā ā ā ā
ā ā āāāāāāāāāāā ā ā setTimeout ā ā callback1 ā ā
ā ā ā func() ā āāāāāāŗā fetch āāāāāāŗā callback2 ā ā
ā ā āāāāāāāāāāā ā ā DOM events ā ā callback3 ā ā
ā ā ā ā ā ā ā ā
ā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāā āāāāāāāā¬āāāāāāāā ā
ā ā² ā ā
ā ā ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā Event Loop ā
ā (moves tasks to stack ā
ā when stack is empty) ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Common Async Operations
| Operation | Description | Example |
|---|---|---|
setTimeout | Delay execution | setTimeout(fn, 1000) |
setInterval | Repeat execution | setInterval(fn, 1000) |
File I/O | Read/write files | fs.readFile(path, cb) |
Network | HTTP requests | fetch, XMLHttpRequest |
Events | User interactions | element.addEventListener() |
Database | Query operations | db.query(sql, cb) |
Callback Patterns
1. Error-First Callbacks (Node.js Convention)
// Error is always the first parameter
function readFile(path, callback) {
// Simulated async operation
setTimeout(() => {
if (path === '') {
callback(new Error('Path is required'), null);
} else {
callback(null, 'File contents here');
}
}, 100);
}
// Usage
readFile('data.txt', function (error, data) {
if (error) {
console.error('Error:', error.message);
return;
}
console.log('Data:', data);
});
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ERROR-FIRST CALLBACK PATTERN ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā callback(error, result) ā
ā ā ā ā
ā ā āāā Success data (null if error) ā
ā ā ā
ā āāā Error object (null if success) ā
ā ā
ā if (error) { ā
ā // Handle error ā
ā return; ā
ā } ā
ā // Use result ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
2. Success/Error Callbacks
function fetchData(url, onSuccess, onError) {
setTimeout(() => {
if (url.includes('error')) {
onError(new Error('Failed to fetch'));
} else {
onSuccess({ data: 'Result' });
}
}, 100);
}
fetchData(
'/api/users',
(data) => console.log('Success:', data),
(error) => console.log('Error:', error.message)
);
Callback Hell (Pyramid of Doom)
Nested callbacks become hard to read and maintain:
// ā Callback Hell - deeply nested, hard to follow
getUser(userId, function (error, user) {
if (error) {
handleError(error);
return;
}
getOrders(user.id, function (error, orders) {
if (error) {
handleError(error);
return;
}
getOrderDetails(orders[0].id, function (error, details) {
if (error) {
handleError(error);
return;
}
getShippingInfo(details.shippingId, function (error, shipping) {
if (error) {
handleError(error);
return;
}
// Finally do something with all the data
displayOrder(user, orders, details, shipping);
});
});
});
});
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā CALLBACK HELL ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā doStep1(function(result1) { ā
ā doStep2(result1, function(result2) { ā
ā doStep3(result2, function(result3) { ā
ā doStep4(result3, function(result4) { ā
ā doStep5(result4, function(result5) { ā
ā // Lost in nesting... ā
ā }); ā
ā }); ā
ā }); ā
ā }); ā
ā }); ā
ā ā
ā Problems: ā
ā ⢠Hard to read ā
ā ⢠Difficult to maintain ā
ā ⢠Error handling repeated everywhere ā
ā ⢠Hard to add new steps ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Avoiding Callback Hell
1. Named Functions
// ā
Extract callbacks into named functions
function handleUser(error, user) {
if (error) return handleError(error);
getOrders(user.id, handleOrders);
}
function handleOrders(error, orders) {
if (error) return handleError(error);
getOrderDetails(orders[0].id, handleDetails);
}
function handleDetails(error, details) {
if (error) return handleError(error);
displayOrder(details);
}
// Start the chain
getUser(userId, handleUser);
2. Modularization
// ā
Create reusable modules
const userModule = {
getWithOrders(userId, callback) {
getUser(userId, (error, user) => {
if (error) return callback(error);
getOrders(user.id, (error, orders) => {
if (error) return callback(error);
callback(null, { user, orders });
});
});
},
};
// Clean usage
userModule.getWithOrders(123, (error, data) => {
if (error) return handleError(error);
console.log(data.user, data.orders);
});
3. Control Flow Libraries
// Using async library (npm package)
async.waterfall(
[
(callback) => getUser(userId, callback),
(user, callback) => getOrders(user.id, callback),
(orders, callback) => getOrderDetails(orders[0].id, callback),
],
(error, result) => {
if (error) return handleError(error);
displayOrder(result);
}
);
Callback Gotchas
1. Callback Called Multiple Times
// ā Bug: callback might be called twice
function badAsync(callback) {
doSomething((error) => {
if (error) {
callback(error);
// Missing return! Continues execution
}
callback(null, 'success');
});
}
// ā
Fixed: ensure single callback
function goodAsync(callback) {
doSomething((error) => {
if (error) {
return callback(error); // Return to stop
}
callback(null, 'success');
});
}
2. Losing this Context
// ā Problem: 'this' is lost
const obj = {
name: 'MyObject',
load(callback) {
setTimeout(function () {
console.log(this.name); // undefined!
callback();
}, 100);
},
};
// ā
Solution 1: Arrow function
const obj1 = {
name: 'MyObject',
load(callback) {
setTimeout(() => {
console.log(this.name); // "MyObject"
callback();
}, 100);
},
};
// ā
Solution 2: Bind
const obj2 = {
name: 'MyObject',
load(callback) {
setTimeout(
function () {
console.log(this.name); // "MyObject"
callback();
}.bind(this),
100
);
},
};
3. Zalgo (Sync/Async Inconsistency)
// ā Bad: Sometimes sync, sometimes async (Zalgo!)
function maybeAsync(value, callback) {
if (cache[value]) {
callback(cache[value]); // Sync!
} else {
fetchValue(value, callback); // Async!
}
}
// ā
Good: Always async
function alwaysAsync(value, callback) {
if (cache[value]) {
setTimeout(() => callback(cache[value]), 0); // Force async
} else {
fetchValue(value, callback);
}
}
// ā
Better: Use setImmediate or process.nextTick (Node.js)
function alwaysAsyncNode(value, callback) {
if (cache[value]) {
process.nextTick(() => callback(cache[value]));
} else {
fetchValue(value, callback);
}
}
Common Callback-Based APIs
setTimeout / setInterval
// One-time delay
const timeoutId = setTimeout(() => {
console.log('Delayed!');
}, 1000);
// Cancel before execution
clearTimeout(timeoutId);
// Repeated execution
const intervalId = setInterval(() => {
console.log('Repeating!');
}, 1000);
// Stop the interval
clearInterval(intervalId);
Event Listeners
// DOM events
button.addEventListener('click', function (event) {
console.log('Button clicked!', event);
});
// Remove listener
function handleClick(event) {
console.log('Clicked!');
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);
Node.js File System
const fs = require('fs');
// Read file (async)
fs.readFile('data.txt', 'utf8', (error, data) => {
if (error) {
console.error('Failed to read:', error);
return;
}
console.log(data);
});
// Write file (async)
fs.writeFile('output.txt', 'Hello!', (error) => {
if (error) {
console.error('Failed to write:', error);
return;
}
console.log('File written!');
});
Callback Utilities
Debounce
// Execute only after delay since last call
function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Usage: only fire after user stops typing
const handleInput = debounce((value) => {
console.log('Searching for:', value);
}, 300);
Throttle
// Execute at most once per interval
function throttle(func, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
func.apply(this, args);
}
};
}
// Usage: limit scroll handler
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 100);
Summary
| Concept | Description |
|---|---|
| Callback | Function passed as argument, called later |
| Sync Callback | Executes immediately (forEach, map) |
| Async Callback | Executes after operation (setTimeout, fetch) |
| Error-First | Convention: callback(error, result) |
| Callback Hell | Deep nesting problem |
| Solutions | Named functions, modularization, Promises |
What's Next?
Callbacks work but have limitations. In the next section, we'll learn about Promises - a more elegant way to handle async operations with better:
- ā¢Error handling
- ā¢Chaining
- ā¢Composition
- ā¢Readability
// Preview: Promises solve callback hell
getUser(userId)
.then((user) => getOrders(user.id))
.then((orders) => getOrderDetails(orders[0].id))
.then((details) => displayOrder(details))
.catch((error) => handleError(error));