What are the pros and cons of using Promises instead of callbacks in JavaScript?
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:
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.
Makes it easy to write sequential asynchronous code that is readable with .then().
The example returns each dependent Promise from a .then() handler, making the next step wait for that operation to complete.
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.
Easier error handling with .catch() and guaranteed cleanup with .finally()
A .catch() at the end of a chain handles rejections propagated through that chain, avoiding a separate error check in every callback.
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.
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()acceptAbortSignal, but calling code must pass and handle it. Promise.all()rejects early without cancelling the remaining operations.- A forgotten
returnorawaitcan 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
- Try implementing your own
Promise.resolve()method,Promise.reject()method andPromise.all()method on GreatFrontEnd.