Callbacks, Promises & Async/Await
Understanding the full async evolution is a core interview signal. Senior engineers don't just prefer async/await for readability â they understand the error propagation model, know when promises compose better, and can articulate callback limitations beyond "callback hell."
Callbacks
A callback is a function passed as an argument to be called later:
function fetchData(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => callback(null, JSON.parse(xhr.responseText));
xhr.onerror = () => callback(new Error('Request failed'));
xhr.send();
}
fetchData('/api/user', (err, user) => {
if (err) {
console.error(err);
return;
}
console.log(user);
});The Node.js Error-First Convention
fs.readFile('data.json', 'utf-8', (err, data) => {
if (err) {
// handle error
return;
}
// use data
});Callback Hell (Pyramid of Doom)
Sequential async operations create deeply nested code:
getUser(userId, (err, user) => {
if (err) return handleError(err);
getOrders(user.id, (err, orders) => {
if (err) return handleError(err);
getOrderDetails(orders[0].id, (err, details) => {
if (err) return handleError(err);
getShippingStatus(details.trackingId, (err, status) => {
if (err) return handleError(err);
render(status);
});
});
});
});Problems beyond nesting:
- Error handling duplicated at every level
- Impossible to run operations in parallel and wait for all
- No built-in cancellation
- Inversion of control â you trust the callee to call your callback correctly
Promises
A Promise represents a future value with three states: pending, fulfilled, rejected:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.5;
success ? resolve('data') : reject(new Error('failed'));
}, 1000);
});
promise
.then(data => console.log(data))
.catch(err => console.error(err))
.finally(() => console.log('done'));Promise Chaining
Each .then() returns a new promise, enabling flat sequential flows:
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => getShippingStatus(details.trackingId))
.then(status => render(status))
.catch(err => handleError(err)); // One catch handles ALL errorsParallel Execution
const [user, posts, notifications] = await Promise.all([
fetchUser(id),
fetchPosts(id),
fetchNotifications(id),
]);Promise Static Methods
| Method | Resolves When | Rejects When |
|---|---|---|
Promise.all | All fulfill | Any rejects |
Promise.race | First settles | First settles |
Promise.any | First fulfills | All reject |
Promise.allSettled | All settle | Never |
// Resilient â get whatever succeeds
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
const successes = results.filter(r => r.status === 'fulfilled').map(r => r.value);
const failures = results.filter(r => r.status === 'rejected').map(r => r.reason);Microtask Scheduling
Promise callbacks (.then, .catch, .finally) run as microtasks â before the next macrotask:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Output: 1, 4, 3, 2Async/Await
Syntactic sugar over promises that makes async code read like synchronous code:
async function loadDashboard(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
const status = await getShippingStatus(details.trackingId);
render(status);
} catch (err) {
handleError(err);
}
}async Function Return Value
An async function always returns a promise:
async function getValue() { return 42; }
getValue(); // Promise<42>
async function throwError() { throw new Error('oops'); }
throwError(); // Promise.reject(Error('oops'))Sequential vs Parallel
// SEQUENTIAL â each waits for the previous (slow)
const user = await fetchUser(id);
const posts = await fetchPosts(id);
const comments = await fetchComments(id);
// PARALLEL â all fire simultaneously (fast)
const [user, posts, comments] = await Promise.all([
fetchUser(id),
fetchPosts(id),
fetchComments(id),
]);Error Handling Patterns
// try/catch (most common)
async function load() {
try {
const data = await fetchData();
return data;
} catch (err) {
return fallbackData;
}
}
// .catch() on the promise (inline)
const data = await fetchData().catch(() => fallbackData);
// Wrapper pattern (Go-style)
async function to(promise) {
try {
const data = await promise;
return [null, data];
} catch (err) {
return [err, null];
}
}
const [err, user] = await to(fetchUser(id));
if (err) return handleError(err);Top-Level Await
ES2022 allows await at the module top level:
// module.js
const config = await fetch('/config.json').then(r => r.json());
export default config;Comparison Table
| Feature | Callbacks | Promises | Async/Await |
|---|---|---|---|
| Readability | Poor (nested) | Good (chained) | Best (sequential) |
| Error handling | Manual at every level | .catch() catches all | try/catch |
| Parallel ops | Manual (counters) | Promise.all | Promise.all + await |
| Cancellation | Not built-in | Not built-in (use AbortController) | Not built-in |
| Debugging | Fragmented stack traces | Better stack traces | Best stack traces |
| Return value | None (via callback) | Promise object | Promise (implicit) |
Common Pitfalls
Forgetting to Await
// BUG: fires and forgets â errors are swallowed
async function save() {
updateDatabase(data); // Missing await!
}
// FIX
async function save() {
await updateDatabase(data);
}Await in Loops
// SEQUENTIAL â each iteration waits (often a bug)
for (const url of urls) {
const data = await fetch(url); // One at a time
}
// PARALLEL â all fire at once
const results = await Promise.all(urls.map(url => fetch(url)));Unhandled Rejections
// This promise rejection will crash Node.js (unhandled)
async function risky() {
throw new Error('oops');
}
risky(); // No .catch(), no try/catch wrapping the caller
// Always handle
risky().catch(handleError);Interview Signal
Senior candidates demonstrate:
- Evolution understanding â Why promises replaced callbacks (inversion of control, error propagation), why async/await improved on promises (readability, debugging)
- Microtask scheduling â Promise callbacks are microtasks, setTimeout is a macrotask, execution order
- Parallel vs sequential â Knows
awaitin a loop is sequential,Promise.allis parallel - Error propagation â One
.catchhandles a chain, try/catch in async/await, unhandled rejection consequences - Practical patterns â
Promise.allSettledfor resilience, Go-styleto()wrapper, AbortController for cancellation