Callbacks & Callback Hell

A callback is just a function you hand to another function to run later. It's the original way JavaScript handled "do this when the work is done" — clicks, timers, file reads, network requests — and it's still everywhere.

Learn Callbacks & Callback Hell in our free JavaScript course — a beginner-friendly interactive lesson with runnable examples, a practice exercise and a…

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

This lesson is your gateway to asynchronous JavaScript. You'll see why callbacks are powerful, how nesting them creates the dreaded "callback hell", and why Promises and async/await came next.

📚 Prerequisites: You need a solid grip on functions (especially passing functions as arguments). This lesson sets up Promises and async/await, which come next.

💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:

📞 Real-World Analogy: A callback is like leaving your number at a busy restaurant:

Because functions are values in JavaScript, you can pass one to another function and let it decide when to call it. You've used this already with map and forEach — those take a callback they run on each item, right away (synchronously).

The interesting case is asynchronous callbacks. With setTimeout , JavaScript schedules your callback and keeps running. The result arrives after the surrounding code has already moved on — which is exactly why the log order surprises beginners.

A classic mistake: trying to return a value out of an async operation. The outer function finishes long before the callback runs, so there's nothing to return yet. The fix is to continue your work inside the callback , or pass another callback in.

Call afterDelay with a function that logs done! .

Node.js made a convention popular: a callback's first argument is an error (or null if all went well), and the data comes after. You check the error first, then proceed. It's simple and consistent — but repeating that check at every level is part of what makes deep nesting painful.

When several async steps must happen in order , each depending on the last, you end up nesting callbacks inside callbacks. The indentation marches to the right and the code gets hard to follow. This shape — the "pyramid of doom" — is callback hell.

You can soften callback hell by giving each step a named function instead of an inline arrow. The nesting flattens and each stage reads as a clear, labelled unit. (The real cure, Promises and async/await , comes in the next lessons — this is the bridge.)

Complete the error-first check so a failure logs the message.

These lines are scrambled. Reorder them so the callback is defined, then passed and run, logging ready .

Why: both functions are declarations (hoisted), so it runs either way — but declaring before the call reads cleanly: define run and the callback, then invoke.

Predict the output before revealing the answer.

a , then c , then b — the setTimeout callback is deferred, so it runs after the synchronous code.

1 then 2 — forEach runs its callback synchronously, once per element, in order.

21 — the callback returns 2 * 10 = 20 , and f adds 1.

Up next: Iterators & Iterables — the protocol behind for...of and spread. 🔄

Practice quiz

What is a callback in JavaScript?

  • A function returned from a Promise
  • A special async keyword
  • A function you pass to another function to be called later
  • A built-in error type

Answer: A function you pass to another function to be called later. A callback is simply a function handed to another function to run at some point, immediately or later.

Are array methods like map and forEach synchronous or asynchronous callbacks?

  • Synchronous
  • Asynchronous
  • Neither
  • Both at once

Answer: Synchronous. map and forEach run their callback right away, once per item, synchronously.

What order do these log: console.log('1'); setTimeout(() => console.log('2'), 0); console.log('3')?

  • 1, 2, 3
  • 3, 2, 1
  • 2, 1, 3
  • 1, 3, 2

Answer: 1, 3, 2. The setTimeout callback is deferred, so it runs after the synchronous code: 1, then 3, then 2.

Why can't you just return a value out of an asynchronous callback's surrounding function?

  • return is forbidden in async code
  • The outer function finishes before the callback runs, so the result isn't ready
  • Callbacks can't return values
  • It causes an infinite loop

Answer: The outer function finishes before the callback runs, so the result isn't ready. The surrounding function returns long before the async callback fires, so there is nothing to return yet.

In the error-first convention, what is the callback's first argument?

  • An error (or null if all went well)
  • The data
  • A status code
  • The callback itself

Answer: An error (or null if all went well). Node popularized callback(err, data): the first argument is an error, or null on success.

What is 'callback hell' (the pyramid of doom)?

  • A runtime crash
  • Calling a callback twice
  • Deeply nested callbacks that march indentation to the right and hurt readability
  • An error in the event loop

Answer: Deeply nested callbacks that march indentation to the right and hurt readability. Nesting callback inside callback for sequential steps creates the hard-to-read pyramid of doom.

What does f return for function f(cb) { return cb(2) + 1; } called as f(x => x * 10)?

  • 20
  • 21
  • 3
  • 30

Answer: 21. The callback returns 2 * 10 = 20, then f adds 1, giving 21.

What is one way to flatten callback hell before using Promises?

  • Use more var declarations
  • Remove all error checks
  • Use synchronous loops
  • Give each step a named function instead of inline arrows

Answer: Give each step a named function instead of inline arrows. Naming each stage as its own function flattens the nesting and makes the flow readable.

What is the difference between setTimeout(doWork, 1000) and setTimeout(doWork(), 1000)?

  • No difference
  • The first passes the function; the second calls doWork now and passes its result
  • The second is faster
  • The first throws an error

Answer: The first passes the function; the second calls doWork now and passes its result. doWork() runs immediately and passes its return value; you want to pass the function itself: doWork.

What replaced callbacks as the modern way to chain asynchronous steps?

  • var and let
  • Synchronous loops
  • Promises and async/await
  • Global variables

Answer: Promises and async/await. Promises and then async/await express the same sequence as flat code with unified error handling.