A Promise is a JavaScript object that represents the eventual result of an asynchronous operation. In browser JavaScript, a Promise stands in for a value that is not available yet, such as a network response, a decoded image or a user permission, and lets code react when the operation succeeds or fails without blocking the main thread.
Promises were standardized in ECMAScript 2015 and are the foundation of modern asynchronous web code. Browser APIs such as the Fetch API, service worker registration and the clipboard API return Promises, and the async/await syntax is built directly on top of them.
How a Promise works
A Promise is always in one of three states. It starts as pending and settles exactly once, either as fulfilled with a value or as rejected with a reason. Once settled, its state never changes, which makes Promises predictable compared with event-based callbacks that might fire several times.
| State | Meaning | Handlers that run |
|---|---|---|
| Pending | Operation still in progress | None yet |
| Fulfilled | Operation succeeded with a value | then callbacks |
| Rejected | Operation failed with a reason, usually an Error | catch callbacks or the second then argument |
Callbacks attached with then, catch and finally are queued as microtasks. They run after the current synchronous code finishes but before the browser handles the next task, such as a timer or an input event. This ordering explains why a resolved Promise callback runs before a setTimeout callback with a delay of zero.
Chaining and error handling
Every call to then returns a new Promise, which makes it possible to build sequential chains. If a callback returns a value, the next Promise fulfills with it; if it returns another Promise, the chain waits for that Promise to settle.
Errors propagate down the chain until a rejection handler catches them, much like exceptions in synchronous code. A single catch at the end of a chain therefore handles failures from any earlier step, while finally runs cleanup code such as hiding a loading indicator regardless of the outcome.
| Method | Purpose | Return value |
|---|---|---|
then | Handle fulfillment, optionally rejection | New Promise from the callback result |
catch | Handle rejection | New Promise, fulfilled if the handler recovers |
finally | Run cleanup in both cases | New Promise that passes through the original outcome |
Rejections without a handler trigger the unhandledrejection event in the browser and appear as errors in the console. Monitoring tools often listen to this event because unhandled rejections are a common source of silent failures.
Combining multiple Promises
Web applications often start several asynchronous operations in parallel. Static combinator methods coordinate them and differ mainly in how they treat failures.
| Combinator | Fulfills when | Rejects when | Typical use |
|---|---|---|---|
Promise.all | All inputs fulfill | Any input rejects | Load required data in parallel |
Promise.allSettled | All inputs settle | Never rejects | Collect results where some may fail |
Promise.race | First input settles | First input settles as rejected | Implement timeouts |
Promise.any | First input fulfills | All inputs reject | Use the fastest of several mirrors |
Promise.all fails fast: one rejected request rejects the whole result, while the other requests continue running in the background. When partial results are acceptable, allSettled is usually the better choice.
Promises compared with callbacks and async/await
Promises replaced deeply nested callbacks as the standard model for asynchronous code in browser JavaScript. The async/await syntax does not replace Promises; it is a more readable way of consuming them.
| Pattern | Readability | Error handling | Notes |
|---|---|---|---|
| Callbacks | Nested and hard to follow | Manual, per callback | Still used by event listeners |
| Promise chains | Linear sequence of steps | One catch for the chain | Explicit and composable |
| Async/await | Reads like synchronous code | try and catch blocks | An async function always returns a Promise |
Promises are designed for a single future value. Repeated events, such as clicks or streaming updates, are better modeled with event listeners, streams or async iterators, because a Promise cannot settle more than once.
Common mistakes with Promises
Most Promise bugs come from lost return values and missing error handlers. The following mistakes appear frequently in code reviews.
- Forgetting to return a Promise inside
then, which breaks the chain and lets later steps run too early. - Missing catch handlers, so failures surface only as unhandled rejection warnings.
- Sequential awaits for independent work, which multiply waiting time instead of running requests in parallel.
- Assuming fetch rejects on HTTP errors: the Fetch API rejects only on network failure, so status codes such as 404 or 500 must be checked explicitly.
- Wrapping existing Promises in a new Promise constructor, an anti-pattern that adds complexity and can hide errors.
Promises also cannot be canceled by themselves. Browser code that must stop pending work, such as an outdated search request, combines Promises with an AbortController signal.
Stripped to the essentials
A Promise is like an order receipt in a restaurant. You do not have your food yet, but the receipt guarantees that you will either get your meal or be told that something went wrong.
While you wait, you can do other things. In the same way, a web page can stay responsive while data loads, and the code that needs the data runs as soon as it arrives.
Where it is applied
A weather web application shows the current conditions, an hourly forecast and air-quality data. Each comes from a separate service. The developer starts all three requests at once with the Fetch API and combines them with Promise.allSettled.
If the air-quality service is slow or fails, the page still displays the weather and forecast and shows a small notice in place of the missing panel. A timeout of eight seconds implemented with an AbortController prevents the request from hanging indefinitely.
Because the requests run in parallel, the page loads in the time of the slowest request instead of the sum of all three, and a failure in one service no longer leaves users looking at a blank screen.