Skip to content

JavaScript Tutorial Asynchronous JavaScript

Callbacks and Their Problems

Lesson 141 of 258 2 min read By Deepak Paswan Updated Oct 4, 2026

Before promises, asynchronous work was handled with callbacks: you hand a function to the slow operation, and it calls you back when it is done. You saw a simple version with setTimeout in the last lessons.

script.js
JS
function loadUser(id, callback) {
  setTimeout(() => {
    callback({ id, name: "Maya" });
  }, 200);
}

loadUser(1, user => {
  console.log("Got user:", user.name); // → Got user: Maya
});
console.log("Waiting…");

Error-first callbacks

A common convention (especially in Node.js) is for the callback to receive an error first, then the result.

script.js
JS
function loadUser(id, callback) {
  setTimeout(() => {
    if (id <= 0) {
      callback(new Error("Invalid id"));
    } else {
      callback(null, { id, name: "Maya" });
    }
  }, 100);
}

loadUser(0, (error, user) => {
  if (error) {
    console.log("Error:", error.message); // → Error: Invalid id
    return;
  }
  console.log(user.name);
});

The problem: nesting

When each step depends on the previous one, callbacks nest deeper and deeper.

script.js
JS
function getUser(cb)      { setTimeout(() => cb(null, { id: 1 }), 50); }
function getPosts(user, cb) { setTimeout(() => cb(null, ["Post A", "Post B"]), 50); }
function getComments(post, cb) { setTimeout(() => cb(null, ["Nice!", "Thanks"]), 50); }

getUser((err, user) => {
  if (err) return console.log(err);
  getPosts(user, (err, posts) => {
    if (err) return console.log(err);
    getComments(posts[0], (err, comments) => {
      if (err) return console.log(err);
      console.log(comments); // → ["Nice!", "Thanks"]
    });
  });
});

This pyramid shape is called callback hell (or the pyramid of doom). The problems:

  • Hard to read, and getting worse with every step.
  • Error handling is repeated at each level.
  • Hard to run steps in parallel, or to wait for several at once.
  • Easy to call the callback twice, or never.

The same flow with promises

script.js
JS
const wait = ms => new Promise(r => setTimeout(r, ms));
const getUser = () => wait(50).then(() => ({ id: 1 }));
const getPosts = user => wait(50).then(() => ["Post A", "Post B"]);
const getComments = post => wait(50).then(() => ["Nice!", "Thanks"]);

getUser()
  .then(getPosts)
  .then(posts => getComments(posts[0]))
  .then(comments => console.log(comments)); // → ["Nice!", "Thanks"]

Flat, readable, with one place for errors. And with async / await it reads like normal code, as you will see.

Callbacks are not bad

Callbacks are still the right tool for events (addEventListener), array methods and many APIs. The trouble is limited to chains of dependent asynchronous steps, which promises handle better.

Next: Promises.