What are Promises and how do they work in JavaScript?
Advertisement
🧩 Scenario
Architecture Walkthrough
Promise States and Transitions
A Promise is an object that represents the eventual result of an asynchronous operation. It exists in one of three states: pending (the operation is in progress), fulfilled (the operation completed with a value), or rejected (the operation failed with a reason). Once a Promise transitions from pending to either fulfilled or rejected, it is settled and cannot change state again.
The executor function passed to new Promise() runs synchronously when the Promise is created. Inside the executor, calling resolve(value) transitions the Promise to fulfilled and calling reject(reason) transitions it to rejected. The callbacks registered with .then(), .catch(), and .finally() always execute asynchronously in the microtask queue, even if the Promise is already settled.
Chaining and Error Propagation
.then() returns a new Promise, which enables chaining. The value returned by a .then callback becomes the resolved value of the next Promise in the chain. If a .then callback returns a Promise instead of a plain value, the chain waits for that inner Promise to settle before continuing.
Errors propagate down the chain automatically. If any .then callback throws or if a Promise rejects, the error skips all subsequent .then callbacks until it reaches a .catch handler. A .catch at the end of a chain handles errors from any point earlier in the chain, which is the idiomatic Promise error handling pattern.
Promise Combinators
Promise.all accepts an array of Promises and returns a single Promise that resolves when all input Promises resolve, with an array of their values. If any input Promise rejects, Promise.all immediately rejects with that reason and ignores all other Promises. This short-circuit behavior makes Promise.all appropriate only when all operations must succeed.
Promise.allSettled waits for all Promises regardless of whether they fulfill or reject. It resolves with an array of result objects, each containing a status field ("fulfilled" or "rejected") and either a value or reason. Use Promise.allSettled when partial failures are acceptable and you need the results of all operations. Promise.race resolves or rejects with the first settled Promise. Promise.any resolves with the first fulfilled Promise and only rejects if all reject.
Key Code Explained
// Creating a Promise
const fetchUser = (id) =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) resolve({ id, name: 'Ghazi' });
else reject(new Error('Invalid ID'));
}, 500);
});
// Chaining .then and .catch
fetchUser(1)
.then((user) => {
console.log(user.name); // 'Ghazi'
return fetchUser(2); // returning a Promise: chain waits for this
})
.then((user2) => console.log(user2.name))
.catch((err) => console.error(err.message)); // catches any rejection in the chain
// Promise.all: all must succeed
Promise.all([fetchUser(1), fetchUser(2), fetchUser(3)])
.then(([u1, u2, u3]) => console.log(u1, u2, u3))
.catch((err) => console.error('At least one failed:', err.message));
// Promise.allSettled: partial failures are fine
Promise.allSettled([fetchUser(1), fetchUser(-1), fetchUser(3)]).then(
(results) => {
results.forEach((r) => {
if (r.status === 'fulfilled') console.log('OK:', r.value.name);
else console.log('Failed:', r.reason.message);
});
},
);
// Promisifying a callback-based API
function readFileAsync(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
The .then chain example demonstrates the key behavior: returning a Promise from inside .then causes the chain to wait for it. This is different from returning a plain value. Forgetting to return (or accidentally returning a plain value when you intended to return a Promise) is one of the most common chaining bugs.
Tradeoffs
| Combinator | Behavior on rejection | Use when |
|---|---|---|
| Promise.all | Short-circuits on first rejection | All operations must succeed |
| Promise.allSettled | Waits for all, collects statuses | Partial failure is acceptable |
| Promise.race | Resolves or rejects with first to settle | Timeout pattern or fastest-wins logic |
| Promise.any | Resolves with first to fulfill | Redundant requests, first success wins |
What Interviewers Actually Check
- Whether you can describe the three states and the one-way transition between them
- Whether you know that
.thenreturns a new Promise and enables chaining - Whether you know that errors propagate down the chain to the nearest
.catch - Whether you understand the difference between
Promise.allandPromise.allSettled - Whether you know that the executor function runs synchronously but
.thencallbacks are always async
Follow-Up Questions
- What is an "unhandled rejection" and how does Node.js handle it by default in recent versions?
- How would you implement
Promise.allyourself using justnew Promiseand a counter? - What is the relationship between
async/awaitand the Promise microtask queue? - If you chain
.then(() => somePromise()).then(result => ...)andsomePromiserejects after 2 seconds, what happens to the outer chain? - A teammate says that wrapping every API call in
new Promise()is fine. When is this an anti-pattern?
Common Candidate Mistakes
- Forgetting to
returna Promise from inside a.thencallback, which breaks the chain and causes the outer Promise to resolve withundefined - Not attaching a
.catchhandler and ignoring unhandled rejection warnings that indicate silent failures - Thinking
Promise.allhandles individual rejections gracefully when it short-circuits on the first one - Wrapping code that already returns a Promise inside
new Promise()unnecessarily (the explicit Promise constructor anti-pattern) - Confusing
Promise.allandPromise.allSettled, using the wrong one when partial failures are expected
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you describe the three Promise states and what transitions exist between them?
- Can you chain
.thencalls correctly, including returning a new Promise from inside.then? - Can you explain the difference between
Promise.allandPromise.allSettled? - Can you write a promisify wrapper that converts a Node.js error-first callback into a Promise?
- Can you trace error propagation through a multi-step Promise chain?
Summary
A Promise is an object representing the eventual fulfillment or rejection of an asynchronous operation. It starts in the pending state and transitions to fulfilled or rejected exactly once. Callbacks registered with .then receive the resolved value; those registered with .catch receive the rejection reason. Both always execute asynchronously in the microtask queue.
Chaining Promises requires returning a value or a new Promise from each .then callback. Returning a Promise causes the chain to wait for it to settle before continuing. Errors propagate automatically through the chain to the nearest .catch, which simplifies error handling for multi-step async sequences compared to nested callbacks.
For multiple concurrent operations, Promise.all is appropriate when all must succeed and one failure should abort the group. Promise.allSettled is appropriate when partial failures are acceptable and you need the result of every operation regardless of outcome.
Do Promises execute immediately?
Yes, the executor function inside new Promise() runs synchronously when the Promise is created. The resolution callbacks (.then) run asynchronously.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement