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.
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.
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.
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
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.