JS Async Callbacks
In this page:
What a Callback Is
A callback is nothing more than a regular function passed as an argument to another function -- the receiving function calls it back at some later point, often once an asynchronous operation (like a timer or a network request) finishes.
Note: Remember a callback is passed as a reference (the bare function name, no parentheses) -- adding parentheses calls it immediately instead of handing it over to be called later.
Warning: Passing myFunction() instead of myFunction as a callback argument executes myFunction immediately and passes its RETURN VALUE as the callback, not the function itself.
Example: What a Callback Is
function greet(name, callback) {
callback(`Hello, ${name}`);
}
greet("Sam", (message) => console.log(message));
Callbacks with Asynchronous Operations
The classic use of a callback is pairing it with something that takes time -- setTimeout(callback, delay) calls the callback after the delay elapses, and older AJAX APIs used callbacks to run code once a network response arrived, since there was no other way to react to something happening "later".
Note: Remember that code immediately after a call using an async callback runs before the callback itself fires -- the callback's scheduling does not pause the rest of the script.
Warning: A callback-based async operation offers no direct way to "wait" for it synchronously -- all dependent logic must live inside the callback itself, or be triggered by it.
Example: Callbacks with Asynchronous Operations
setTimeout(() => console.log("Runs later"), 500);
// Older AJAX used callbacks similarly, run once a response arrives
The Error-First Callback Convention
A widely-adopted convention, especially in Node.js-style code, is the "error-first" callback: callback(error, result), where the first argument is either an Error object (if something went wrong) or null (if it succeeded) -- checking that first argument becomes the standard way to handle failure in callback-based code.
Note: Always check the error argument first inside an error-first callback, before touching the result argument, since the result is meaningless (or absent) when an error occurred.
Warning: Skipping the error check and assuming every callback invocation represents success is a common source of bugs in callback-based code, since errors are easy to silently ignore this way.
Example: The Error-First Callback Convention
function loadData(callback) {
setTimeout(() => callback(null, { id: 1 }), 100);
}
loadData((error, result) => {
if (error) console.log("Failed:", error);
else console.log("Success:", result);
});
The Problem: Callback Hell
When several asynchronous steps must happen in sequence, each depending on the previous one's result, callback-based code nests deeper and deeper -- a pattern commonly called 'callback hell' or 'the pyramid of doom', which becomes genuinely hard to read, modify, and debug as more steps are added.
Note: Recognize deeply nested callbacks as a signal that Promises or async/await would express the same logic far more readably.
Warning: Error handling becomes especially painful in deeply nested callbacks, since each level typically needs its own separate error check, multiplying the boilerplate at every level of nesting.
Example: The Problem: Callback Hell
function step1(cb) { setTimeout(() => cb(1), 10); }
function step2(a, cb) { setTimeout(() => cb(a + 1), 10); }
function step3(b, cb) { setTimeout(() => cb(b + 1), 10); }
step1((a) => {
step2(a, (b) => {
step3(b, (c) => console.log("Deeply nested result:", c)); // callback hell
});
});
Callbacks Still in Use Today
Despite Promises and async/await being preferred for most sequential async logic, plain callbacks remain common and appropriate for certain patterns -- event listeners (addEventListener), array iteration methods (forEach, map), and any 'run this every time X happens' scenario that is not really about a single eventual result.
Note: Recognize that callbacks are not "deprecated" -- they remain the right tool for repeated or event-driven invocations, while Promises fit better for a single eventual async result.
Warning: Trying to force an event listener (which can fire many times) into a Promise-based pattern (designed for a single resolution) is usually a poor fit -- callbacks remain the natural choice there.
Example: Callbacks Still in Use Today
document.addEventListener("click", () => console.log("Clicked")); // still a normal, appropriate callback use
[1, 2, 3].forEach(n => console.log(n)); // another normal callback use
- Nesting callback inside callback inside callback for a sequence of dependent async steps, producing deeply indented, hard-to-follow code often called 'callback hell'.
- Following the error-first callback convention inconsistently, forgetting to check the error argument before using the result argument.
- Calling a callback function immediately (with parentheses) instead of passing a reference to it, executing it right away instead of scheduling it for later.
- A callback is simply a function passed as an argument to another function, to be invoked later once some operation completes.
- The error-first convention (callback(error, result)) is a common pattern where the first argument is either an error object or null.
- Deeply nested callbacks for sequential async steps produce hard-to-read "callback hell", which Promises and async/await were designed to solve.
The callback pattern itself is a basic JavaScript language feature, supported since the language's creation, and works in every environment.
Chapter Quiz — Complete all 26 topics to unlock
0/26 topics done
Complete these topics first:
- JS Dates
- JS Math
- JS Conditionals
- JS Switch
- JS Loop For
- JS Loop While
- JS Iterables
- JS Sets
- JS Maps
- JS typeof
- JS Type Conversion
- JS Destructuring
- JS Arrow Functions
- JS Classes
- JS Modules
- JS Promises
- JS Async/Await
- JS DOM
- JS DOM Methods
- JS Events Advanced
- JS DOM Navigation
- JS DOM Collections
- JS Async Callbacks
- JS Async Parallel
- JS Date Set
- JS Set Logic