Before Promises, asynchronous JavaScript meant nested callbacks — and deeply nested async logic (“callback hell”) was infamous for becoming unreadable fast.
// Callback hell: each step nests one level deeper
getUser(userId, (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0].id, (details) => {
console.log(details); // four levels deep and growing
}, handleError);
}, handleError);
}, handleError);
// Promises: flat chains instead of nesting
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => console.log(details))
.catch(handleError); // ONE error handler for the whole chain
Promises’ biggest practical win over callbacks isn’t just readability — it’s that error handling collapses to a single .catch() at the end of the chain, instead of needing an error callback threaded through every single nested step.