Quiz

What are the pros and cons of using Promises instead of callbacks in JavaScript?

Topics
AsyncJavaScript

TL;DR

Promises standardize one eventual outcome and make sequential, parallel, and error flows composable with .then(), async/await, and combinators such as Promise.all(). They avoid many callback-contract ambiguities, but they do not cancel work, represent repeated events, or guarantee settlement. A Promise chain can still become unreadable or leak an unhandled rejection when callers forget to return or await it.


Pros

Avoid callback hell which can be unreadable.

Callback hell, also known as the "pyramid of doom," is a phenomenon that occurs when you have multiple nested callbacks in your code. This can lead to code that is difficult to read, maintain, and debug. Here's an example of callback hell:

function getFirstData(callback) {
setTimeout(() => {
callback({ id: 1, title: 'First Data' });
}, 1000);
}
function getSecondData(data, callback) {
setTimeout(() => {
callback({ id: data.id, title: data.title + ' Second Data' });
}, 1000);
}
function getThirdData(data, callback) {
setTimeout(() => {
callback({ id: data.id, title: data.title + ' Third Data' });
}, 1000);
}
// Callback hell
getFirstData((data) => {
getSecondData(data, (data) => {
getThirdData(data, (result) => {
console.log(result); // Output: {id: 1, title: "First Data Second Data Third Data"}
});
});
});

Promises address the problem of callback hell by providing a more linear and readable structure for your code.

Note that this is just for illustration. The cleanest modern approach is to use async/await, which builds on Promises.

// Example of sequential asynchronous code using setTimeout and Promises
function getFirstData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: 1, title: 'First Data' });
}, 1000);
});
}
function getSecondData(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: data.id, title: data.title + ' Second Data' });
}, 1000);
});
}
function getThirdData(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: data.id, title: data.title + ' Third Data' });
}, 1000);
});
}
getFirstData()
.then(getSecondData)
.then(getThirdData)
.then((data) => {
console.log(data); // Output: {id: 1, title: "First Data Second Data Third Data"}
})
.catch((error) => console.error('Error:', error));

Makes it easy to write sequential asynchronous code that is readable with .then().

In the above code example, we use the .then() method to chain these Promises together, allowing the code to execute sequentially. It provides a cleaner and more manageable way to handle asynchronous operations in JavaScript.

Makes it easy to write parallel asynchronous code with Promise.all().

Both Promise.all() and callbacks can be used to write parallel asynchronous code. However, Promise.all() provides a more concise and readable way to handle multiple Promises, especially when dealing with complex asynchronous workflows.

function getData1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: 1, title: 'Data 1' });
}, 1000);
});
}
function getData2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: 2, title: 'Data 2' });
}, 1000);
});
}
function getData3() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: 3, title: 'Data 3' });
}, 1000);
});
}
Promise.all([getData1(), getData2(), getData3()])
.then((results) => {
console.log(results); // Output: [{ id: 1, title: 'Data 1' }, { id: 2, title: 'Data 2' }, { id: 3, title: 'Data 3' }]
})
.catch((error) => {
console.error('Error:', error);
});

Easier error handling with .catch() and guaranteed cleanup with .finally()

Promises make error handling more straightforward by allowing you to catch errors at the end of a chain using .catch(), instead of manually checking for errors in every callback. This leads to cleaner and more maintainable code.

Additionally, .finally() lets you run code after the Promise settles, whether it succeeded or failed, which is great for cleanup tasks like hiding spinners or resetting UI states.

function getFirstData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: 1, title: 'First Data' });
}, 1000);
});
}
function getSecondData(data) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: data.id, title: data.title + ' -> Second Data' });
}, 1000);
});
}
getFirstData()
.then(getSecondData)
.then((data) => {
console.log('Success:', data);
})
.catch((error) => {
console.error('Error:', error);
})
.finally(() => {
console.log('This runs no matter what');
});

A stricter settlement contract

A Promise settles at most once, and reactions always run asynchronously as microtasks. This prevents a producer from delivering two results or sometimes invoking a consumer synchronously and sometimes asynchronously. However, the Promise can still remain pending forever, settle later than useful, or wrap an operation that keeps running after the result is ignored.

Cons

  • Promises model one future result, not repeated events or a stream of values.
  • Cancellation is separate. APIs such as fetch() accept AbortSignal, but calling code must pass and handle it.
  • Promise.all() rejects early without cancelling the remaining operations.
  • A forgotten return or await can detach work and its errors from the caller.
  • Long chains can obscure which operation failed unless errors include useful context.
  • Converting a callback API to a Promise is safe only when the callback is expected to produce one result. Event listeners and subscriptions need a different abstraction.

Practice

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise
Check your understanding Exercise

Which statements correctly describe Promises compared with callback APIs? Select all that apply.