The Three States of a Promise
A Promise is always in one of three states. Once it leaves pending, it is "settled" and can never change state again.
| State | Meaning | Triggered by | Can transition to |
|---|---|---|---|
| Pending | Initial state β not yet settled | Creation via new Promise() | Fulfilled or Rejected |
| Fulfilled | Operation completed successfully | Calling resolve(value) | Never changes again |
| Rejected | Operation failed | Calling reject(reason) or throwing inside executor | Never changes again |
// Creating a Promise
const p1 = new Promise((resolve, reject) => {
// Executor runs synchronously right away
setTimeout(() => {
const success = Math.random() > 0.3;
if (success) {
resolve({ data: 'Hello World', status: 200 });
} else {
reject(new Error('Server error'));
}
}, 500);
});
// Observing the state via .then() and .catch()
p1
.then(result => {
console.log('Fulfilled:', result.data);
console.log('Status:', result.status);
})
.catch(err => {
console.error('Rejected:', err.message);
})
.finally(() => {
console.log('Always runs β cleanup here');
});
console.log('Promise is still pending here...');
Promise is still pending here... Fulfilled: Hello World β or Rejected: Server error Status: 200 β if fulfilled Always runs β cleanup here
then() Chaining
.then() always returns a new Promise. This lets you chain operations: each .then() receives the resolved value of the previous one. You can transform the data at each step.
function delay(ms, value) {
return new Promise(resolve => setTimeout(() => resolve(value), ms));
}
delay(100, 5)
.then(n => {
console.log('Step 1:', n); // 5
return n * 2; // pass 10 to next .then
})
.then(n => {
console.log('Step 2:', n); // 10
return delay(100, n + 5); // return another Promise
})
.then(n => {
console.log('Step 3:', n); // 15
return n.toString();
})
.then(str => {
console.log('Step 4:', str, typeof str); // '15' string
});
Step 1: 5 Step 2: 10 Step 3: 15 Step 4: 15 string
catch() and finally()
.catch(fn) is shorthand for .then(undefined, fn). It catches any rejection anywhere earlier in the chain. .finally(fn) runs regardless of success or failure and is ideal for cleanup (hiding spinners, closing connections).
function fetchUser(id) {
return new Promise((resolve, reject) => {
if (id <= 0) {
reject(new TypeError('ID must be positive'));
return;
}
setTimeout(() => resolve({ id, name: 'Alice', role: 'admin' }), 100);
});
}
// Error propagates through .then() chain to the first .catch()
fetchUser(-1)
.then(user => {
console.log(user.name); // skipped β rejected
return user.role;
})
.then(role => {
console.log(role); // also skipped
})
.catch(err => {
console.error('Caught:', err.message);
return 'default-role'; // recover β chain resumes
})
.then(role => {
console.log('Recovered with:', role);
})
.finally(() => {
console.log('Cleanup complete');
});
Caught: ID must be positive Recovered with: default-role Cleanup complete
Static Combinators: all, allSettled, race, any
Promise provides four static methods for coordinating multiple Promises:
const slow = new Promise(res => setTimeout(() => res('slow'), 300));
const fast = new Promise(res => setTimeout(() => res('fast'), 100));
const failed = new Promise((_, rej) => setTimeout(() => rej(new Error('boom')), 200));
// Promise.all β resolves when ALL resolve; rejects on first rejection
Promise.all([slow, fast])
.then(results => console.log('all:', results)); // ['slow', 'fast']
// Promise.allSettled β waits for ALL; never rejects
Promise.allSettled([slow, fast, failed])
.then(results => results.forEach(r => console.log('settled:', r.status, r.value ?? r.reason?.message)));
// Promise.race β first to SETTLE (resolve OR reject) wins
Promise.race([slow, fast, failed])
.then(v => console.log('race winner:', v)) // 'fast'
.catch(e => console.log('race error:', e.message));
// Promise.any β first to FULFILL (ignores rejections); rejects only if ALL fail
Promise.any([failed, fast, slow])
.then(v => console.log('any winner:', v)) // 'fast'
.catch(e => console.log('all failed:', e.message));
all: ['slow', 'fast'] race winner: fast any winner: fast settled: fulfilled slow settled: fulfilled fast settled: rejected boom
Use Promise.all when all operations must succeed (e.g., fetching required data). Use Promise.allSettled when you want all results regardless of failure (e.g., batch notifications). Use Promise.race for timeout patterns. Use Promise.any for fallback sources.
Promise.resolve() and Promise.reject() Shortcuts
// Pre-resolved Promise β useful for uniform return types
function getUser(id) {
if (id === 0) return Promise.resolve({ id: 0, name: 'Guest' }); // sync path wrapped
return fetch(`/api/users/${id}`).then(r => r.json()); // async path
}
// Pre-rejected Promise β useful for early validation
function divide(a, b) {
if (b === 0) return Promise.reject(new Error('Division by zero'));
return Promise.resolve(a / b);
}
divide(10, 2).then(v => console.log(v)); // 5
divide(10, 0).catch(e => console.log(e.message)); // Division by zero
5 Division by zero
Promises and the Microtask Queue
Promise callbacks (.then, .catch, .finally) are placed in the microtask queue, not the regular (macro)task queue. The event loop drains all microtasks after every macrotask, meaning Promise callbacks always run before the next setTimeout callback.
console.log('1: synchronous start');
setTimeout(() => console.log('4: setTimeout (macrotask)'), 0);
Promise.resolve()
.then(() => console.log('3: first microtask'))
.then(() => console.log('3b: second microtask'));
console.log('2: synchronous end');
// Order: 1 β 2 β 3 β 3b β 4
1: synchronous start 2: synchronous end 3: first microtask 3b: second microtask 4: setTimeout (macrotask)
If a rejected Promise has no .catch() handler, Node.js (v15+) and modern browsers will crash the process or log a warning. Always attach a .catch() to every Promise chain, or use try/catch with async/await.
ποΈ Practical Exercise
Build a retry(fn, maxAttempts, delayMs) utility that:
- Calls the async function
fn()(returns a Promise). - If it rejects, waits
delayMsmilliseconds and retries. - Retries up to
maxAttemptstimes total. - If all attempts fail, rejects with the last error.
- Test with a function that fails the first two times then succeeds.
π₯ Challenge Exercise
Implement a PromisePool(concurrency) class that processes an array of async tasks with a maximum concurrency limit. For example, with concurrency=3 and 10 tasks, at most 3 run simultaneously at any time. Use only Promises (no async/await). This pattern is essential for rate-limiting API calls.
Interview Questions
- What are the three states of a Promise and can a settled Promise change state?
- What does
.then()return and why does that enable chaining? - What is the difference between Promise.all() and Promise.allSettled()?
- What is the difference between Promise.race() and Promise.any()?
- Why do Promise callbacks run before setTimeout callbacks of the same delay?
- How do you handle errors in a Promise chain?
π Summary
- A Promise has three states: pending, fulfilled, rejected. Settled Promises never change state.
.then()returns a new Promise β enabling chaining and data transformation..catch()handles any rejection earlier in the chain;.finally()always runs for cleanup.Promise.all: all must succeed.Promise.allSettled: wait all.Promise.race: first to settle.Promise.any: first to fulfil.- Promise callbacks go to the microtask queue, which is drained before the next macrotask.
Frequently Asked Questions
Yes. If you call resolve(anotherPromise), the outer Promise "follows" the inner one β it will adopt the inner Promise's state when it settles. This is called "assimilation" and it's why returning a Promise from .then() flattens the chain instead of creating a nested Promise-of-Promise.
Throwing inside a .then() callback rejects the Promise returned by that .then(). The error propagates down the chain to the next .catch() handler, just as if you had called reject(new Error(...)) explicitly.
Promise.race resolves or rejects with the first Promise to settle β whether it fulfils or rejects. Promise.any only resolves with the first Promise to fulfil; it ignores rejections unless all Promises reject (in which case it throws an AggregateError).