JavaScript Promises Explained: then, catch, Promise.all and Common Mistakes
Promises are how JavaScript handles work that finishes later, and async/await is built on top of them. Learn how they really behave, with real output from each example, and avoid the mistakes that swallow your errors.
- A promise is an object representing a result that will be available later.
- It is pending until it is either fulfilled or rejected, and once settled it never changes.
- Each
thenreturns a new promise, which is what makes chaining possible. - Use
Promise.allwhen every result is required, andPromise.allSettledwhen you want every outcome.
Network requests, file reads and timers all finish later, but JavaScript cannot simply stop and wait, or the page would freeze. JavaScript promises give you a tidy way to say “when this finishes, do that, and if it fails, do this instead.”
Even if you mostly write async and await, you are using promises underneath, so understanding them makes debugging far easier. We ran these examples on Node.js 24, and the output shown comes from that run. They behave the same way in modern browsers.
What is a promise?
A promise is a placeholder for a future value. Think of the buzzer a restaurant hands you when you order. You do not have your food yet, but you hold something that will tell you when it is ready, or that there is a problem, and meanwhile you are free to do other things.
In code, an asynchronous function returns a promise immediately, and you attach what should happen next:
fetch("/api/user")
.then((response) => response.json())
.then((user) => console.log(user.name))
.catch((error) => console.log("Failed:", error.message));
The three promise states
| State | Meaning | What runs |
|---|---|---|
| Pending | The work has not finished | Nothing yet |
| Fulfilled | It finished with a value | then handlers |
| Rejected | It failed with a reason | catch handlers |
A promise that is fulfilled or rejected is called settled. A promise can settle only once. After that, its state and value are fixed, and later calls to resolve or reject are ignored.
Create a promise
You usually consume promises from APIs like fetch, but creating one shows how they work. The constructor receives two functions, resolve and reject:
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: "Asha" });
} else {
reject(new Error("Invalid id"));
}
}, 100);
});
}
console.log(fetchUser(1));
// Promise { <pending> }
Logging the promise right away shows it is still pending, because the timer has not fired. The value can only be reached through a handler or await. Always reject with an Error object rather than a string, since an Error carries a stack trace that helps with debugging.
then, catch and finally
These three methods attach handlers to a promise:
fetchUser(1).then((user) => console.log(user.name));
// Asha
fetchUser(-1)
.then((user) => console.log("never runs", user))
.catch((error) => console.log("caught:", error.message))
.finally(() => console.log("finally"));
// caught: Invalid id
// finally
then runs on success, catch runs on failure and finally runs either way. That makes finally the right place for cleanup, such as hiding a loading spinner. It receives no arguments, because it should not care about the outcome.
Chaining promises
The key idea is that then always returns a new promise. Whatever your handler returns becomes that new promise’s value:
Promise.resolve(2)
.then((n) => n * 10)
.then((n) => n + 1)
.then((n) => console.log("chain:", n));
// chain: 21
If a handler returns another promise, the chain waits for it to settle before moving on. This is what turns nested callbacks into a flat sequence of steps:
getUser(id)
.then((user) => getOrders(user.id))
.then((orders) => getInvoice(orders[0].id))
.then((invoice) => console.log(invoice.total))
.catch(handleError);
How errors travel through a chain
A rejected promise skips every then until it reaches a catch. Throwing inside a handler counts as a rejection too. And a catch that returns normally gets the chain back on track:
Promise.resolve("start")
.then(() => {
throw new Error("step 2 failed");
})
.then(() => console.log("skipped"))
.catch((error) => {
console.log("caught:", error.message);
return "recovered";
})
.then((value) => console.log("continues:", value));
// caught: step 2 failed
// continues: recovered
Think of this as the asynchronous version of try and catch. One catch at the end usually covers the whole chain. If you only want to log an error and still fail, throw it again from inside the catch.
Tip: fetch rejects only for network failures. A 404 or 500 response still fulfills, so check response.ok yourself. Our HTTP status codes cheat sheet explains what each code means.
Promise.all, allSettled, race and any
These static methods coordinate several promises at once. All of them take an array and return one promise.
| Method | Fulfills when | Rejects when | Use it for |
|---|---|---|---|
Promise.all |
All fulfill | Any one rejects | All results are required |
Promise.allSettled |
All settle | Never | You want every outcome |
Promise.race |
The first to settle fulfills | The first to settle rejects | Timeouts |
Promise.any |
The first fulfills | All reject | The first successful source |
To test them, we used two small helpers, one that succeeds after a delay and one that fails after a delay:
const wait = (ms, value) =>
new Promise((resolve) => setTimeout(() => resolve(value), ms));
const fail = (ms, message) =>
new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms));
Promise.all
await Promise.all([wait(300, "a"), wait(100, "b"), wait(200, "c")]);
// [ 'a', 'b', 'c' ] took about 300 ms
Two details to notice. Results keep the input order, even though b finished first. And the total time was about 300 ms, the time of the slowest task and not the 600 ms sum, because all three were already running at once. If any one of them rejects, the whole thing rejects straight away:
await Promise.all([wait(100, "a"), fail(50, "b failed")]);
// rejected: b failed
Promise.allSettled
This waits for everything and reports each result, so one failure does not lose the rest. It suits a dashboard where each widget loads independently:
const results = await Promise.allSettled([wait(50, "a"), fail(50, "b failed")]);
results.map((r) => r.status);
// [ 'fulfilled', 'rejected' ]
Promise.race and Promise.any
await Promise.race([wait(300, "slow"), wait(100, "fast")]); // fast
await Promise.any([fail(50, "x"), wait(100, "ok")]); // ok
The difference is that race takes the first to settle, even if it failed, while any ignores failures as long as something succeeds. A classic use of race is adding a timeout to a slow request, though the slower task keeps running in the background unless you cancel it.
Promises and async/await
async and await are a more readable way to work with the same promises. An async function always returns a promise, and await pauses that function until a promise settles:
async function showInvoice(id) {
try {
const user = await getUser(id);
const orders = await getOrders(user.id);
console.log(orders.length);
} catch (error) {
console.log("Failed:", error.message);
}
}
Rejections become thrown errors, so ordinary try and catch works. You still need Promise.all for parallel work, because awaiting one call after another runs them in sequence. Our JavaScript async/await guide goes deeper on this.
Promises and the event loop
Promise handlers are never called immediately, even when the promise has already settled. They run after the current code finishes, and before any timers. Here is the proof:
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("sync");
// sync
// microtask
// timeout
Synchronous code runs first. Next comes the microtask queue, which holds promise handlers. Only then does the event loop pick up the next task, such as a timer. This is a popular interview question, and it explains why a setTimeout of zero is not really immediate.
Promises vs callbacks
| Concern | Callbacks | Promises |
|---|---|---|
| Sequencing | Nested, deeper at each step | A flat chain |
| Errors | Checked at every level | One catch can cover the chain |
| Parallel work | Manual counters | Promise.all |
| Called twice by mistake? | Possible | Cannot settle twice |
Common mistakes
- Forgetting to return inside
then. Without areturn, the next step receivesundefinedand does not wait for your inner promise. - No
catchat all. This causes an unhandled rejection, and in Node.js that stops the process by default. - Nesting instead of chaining. Putting
theninsidethenrecreates callback hell. Return the promise and continue the chain. - Wrapping a promise in
new Promise. If a function already returns a promise, just use it. - Using
forEachwith async callbacks. It does not wait. Usefor...oforPromise.allwithmap, as shown in our guide to JavaScript array methods. - Thinking a promise can be cancelled. It cannot on its own. For requests, use
AbortController.
Final thoughts
Promises come down to a few rules: a promise settles once, then returns a new promise, and errors travel down the chain until something catches them. Once those rules feel natural, async and await stop being magic. To practise, rewrite one callback-based function using promises, then fetch two endpoints with Promise.all and handle a failure properly.
Frequently asked questions
What is a promise in JavaScript?
A promise is an object representing the eventual result of asynchronous work. It starts as pending, then either fulfills with a value or rejects with a reason.
What is the difference between then and catch?
then runs when a promise fulfills, and catch runs when it rejects. catch is effectively then with only a rejection handler, and it can also catch errors thrown in earlier steps.
What is the difference between Promise.all and Promise.allSettled?
Promise.all rejects as soon as one promise rejects. Promise.allSettled always waits for all of them and reports each one’s status and value or reason.
Should I use promises or async/await?
They work together, as await is built on promises. Most code reads best with async/await, while Promise.all and similar methods remain useful for parallel work.
Do promises run in parallel?
JavaScript runs one piece of code at a time on a thread, but work like network requests happens in the background. Several of these can be in progress at once.
Keep learning
Need help with testing, WordPress or SEO?
We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.