// js · Web Platform Advent #2
JavaScript Promises: a practical guide
Understand JavaScript promises — creating them, then/catch/finally, chaining, and combining many with Promise.all, race, any and allSettled — with runnable examples and the common gotchas.
A promise is an object that represents a value that isn't ready yet. It's the foundation of asynchronous JavaScript: fetch(), timers, file reads in Node, and most modern APIs return promises. A promise is always in one of three states — pending, fulfilled, or rejected — and once it settles it never changes again.
Creating a promise
Most of the time you consume promises that an API hands you. But you create one yourself with the Promise constructor, which takes an executor function receiving resolve and reject:
const wait = (ms) => new Promise((resolve) => {
setTimeout(resolve, ms);
});
const fetchUser = (id) => new Promise((resolve, reject) => {
if (!id) reject(new Error('id is required'));
else resolve({ id, name: 'Ada' });
}); Calling resolve(value) fulfils the promise with that value; calling reject(error) rejects it. Anything thrown synchronously inside the executor also rejects the promise.
then, catch and finally
You read a promise's outcome with .then() for success, .catch() for failure, and .finally() for cleanup that runs either way:
fetchUser(42)
.then((user) => console.log('Got', user.name))
.catch((err) => console.error('Failed:', err.message))
.finally(() => console.log('Done')); .then() can take two arguments — onFulfilled and onRejected — but a separate .catch() at the end of the chain is clearer and also catches errors thrown inside earlier .then() handlers.
Chaining: the real superpower
Every .then() returns a new promise, so they chain. Whatever you return from a handler becomes the input to the next one. Crucially, if you return a promise, the chain waits for it to settle before continuing:
fetch('/api/user')
.then((res) => res.json()) // returns a promise → chain waits
.then((user) => fetch('/api/posts?user=' + user.id))
.then((res) => res.json())
.then((posts) => console.log(posts.length, 'posts'))
.catch((err) => console.error(err)); A single .catch() at the end handles a rejection from any step above it, which is why chaining beats nesting callbacks.
Running promises together
When tasks don't depend on each other, start them in parallel and combine the results. Four static methods cover the common cases:
Promise.all([...])— waits for all to fulfil; rejects as soon as one rejects. Returns an array of results in order.Promise.allSettled([...])— waits for all to finish and never rejects; returns an array of{ status, value }or{ status, reason }objects.Promise.race([...])— settles with the first promise to settle, whether it fulfils or rejects.Promise.any([...])— settles with the first promise to fulfil; only rejects if they all reject.
// Fetch three resources at once, fail if any fails
const [user, posts, prefs] = await Promise.all([
fetch('/api/user').then((r) => r.json()),
fetch('/api/posts').then((r) => r.json()),
fetch('/api/prefs').then((r) => r.json()),
]);
// Same, but tolerate individual failures
const results = await Promise.allSettled([taskA(), taskB(), taskC()]);
results.forEach((r) => {
if (r.status === 'fulfilled') console.log('ok', r.value);
else console.warn('failed', r.reason);
}); Common gotchas
- Forgetting to return. Inside a
.then(), if you call a promise but don'treturnit, the chain won't wait for it — you lose ordering and error handling. - Unhandled rejections. A promise without a
.catch()(or atry/catcharoundawait) produces anunhandledrejection. Always handle errors at the end of the chain. - Mixing callbacks and promises. Don't call
resolvetwice or bothresolveandreject— only the first call counts; the rest are silently ignored. - The executor runs immediately. The function you pass to
new Promise()runs synchronously, right away — only the.then()callbacks are deferred.
Quick reference
| Goal | Method |
|---|---|
| Handle success | .then(onFulfilled) |
| Handle failure | .catch(onRejected) |
| Cleanup either way | .finally(fn) |
| All must succeed | Promise.all |
| Collect every outcome | Promise.allSettled |
| First to settle | Promise.race |
| First to succeed | Promise.any |
Promises are the plumbing under async/await, which is just nicer syntax over the same objects. Once chaining and the four combinators feel natural, asynchronous JavaScript stops being intimidating.