← Back to JavaScript Course | Chapter 4: Modern JS, Async & DOM | Lesson 23 of 26

JS Async Callbacks

Before Promises and async/await existed, asynchronous JavaScript relied entirely on callback functions -- a function passed as an argument, invoked later once an operation completes. Understanding callbacks remains essential both for reading older code and for appreciating exactly what problem Promises were designed to solve.

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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
document.addEventListener("click", () => console.log("Clicked")); // still a normal, appropriate callback use
[1, 2, 3].forEach(n => console.log(n)); // another normal callback use
Common Mistakes
  1. Nesting callback inside callback inside callback for a sequence of dependent async steps, producing deeply indented, hard-to-follow code often called 'callback hell'.
  2. Following the error-first callback convention inconsistently, forgetting to check the error argument before using the result argument.
  3. Calling a callback function immediately (with parentheses) instead of passing a reference to it, executing it right away instead of scheduling it for later.
Chapter Summary
  • 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.
Browser Support

The callback pattern itself is a basic JavaScript language feature, supported since the language's creation, and works in every environment.

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.