Loading...
Loading...
The original async pattern. Pass a function to be called when work is done.
Browsers and servers spend most of their time waiting — for network responses, timers, and user input. Async JavaScript lets your program stay responsive while work happens in the background using callbacks, Promises, and async/await.
Learn Async Callbacks in JavaScript — covering Async Callbacks, Error-First Pattern, Callback Hell, Fixing Callback Hell with interactive visual examples and step-by-step explanations.
Synchronous code runs one line at a time. But real applications wait for network requests, file reads, and timers. Async JavaScript lets your program keep running while that work happens in the background — without freezing the UI.
This module covers the full async story: callbacks, Promises, async/await, and the Fetch API. Visual timelines show when tasks enter the queue, when they resolve, and why async/await is syntactic sugar over Promises — not magic.
Understanding async callbacks is essential for every JavaScript developer. It shows up in frontend UI code, backend APIs, and framework internals — and getting it wrong leads to bugs that are hard to trace. This chapter builds intuition with visuals so the behavior sticks.
A function you pass that gets called when an async operation completes.
// Synchronous callback (runs immediately): [1, 2, 3].forEach(n => console.log(n)); // Asynchronous callback (runs LATER): setTimeout(() => { console.log("I run after 1 second"); }, 1000);
The key difference: async callbacks don't run immediately. They're stored and called when the async operation finishes.
Node.js convention: first argument is always the error.
// Error-first callback pattern: function readFile(path, callback) { setTimeout(() => { if (path === "") { callback(new Error("Path is empty"), null); } else { callback(null, "file contents here"); } }, 100); } // Usage — always check error first: readFile("data.txt", (error, data) => { if (error) { console.error("Failed:", error.message); return; // stop here! } console.log("Got:", data); });
Convention:
callback(error, result)
• If error is null → success, use result
• If error exists → failure, handle error
Nested callbacks become unreadable. The 'Pyramid of Doom'.
// The problem: sequential async operations 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); getShipping(details.trackingId, (err, shipping) => { if (err) return handleError(err); // 😱 4 levels deep! And it gets worse... displayShipping(shipping); }); }); }); });
Problems with callback hell:
Named functions and modularization help — but Promises are the real solution.
// Fix 1: Named functions (flatten the pyramid) function handleUser(err, user) { if (err) return handleError(err); getOrders(user.id, handleOrders); } function handleOrders(err, orders) { if (err) return handleError(err); getOrderDetails(orders[0].id, handleDetails); } function handleDetails(err, details) { if (err) return handleError(err); getShipping(details.trackingId, displayShipping); } // Start the chain: getUser(userId, handleUser);
// Fix 2: Promises (the modern solution) getUser(userId) .then(user => getOrders(user.id)) .then(orders => getOrderDetails(orders[0].id)) .then(details => getShipping(details.trackingId)) .then(shipping => displayShipping(shipping)) .catch(handleError); // one error handler! // Fix 3: Async/Await (cleanest) async function showShipping(userId) { const user = await getUser(userId); const orders = await getOrders(user.id); const details = await getOrderDetails(orders[0].id); const shipping = await getShipping(details.trackingId); displayShipping(shipping); }
Where you still see callbacks today.
// Event listeners (always callbacks): button.addEventListener("click", () => { console.log("Clicked!"); }); // Array methods: [1, 2, 3].map(n => n * 2); // setTimeout / setInterval: setTimeout(() => console.log("later"), 1000); // Node.js fs (legacy API): const fs = require("fs"); fs.readFile("file.txt", "utf8", (err, data) => { if (err) throw err; console.log(data); }); // Geolocation API: navigator.geolocation.getCurrentPosition( (pos) => console.log(pos.coords), (err) => console.error(err) );
Callbacks aren't dead — event listeners, array methods, and many APIs still use them. But for sequential async operations, Promises and async/await are far better.
Common questions about async callbacks.