Promises in JavaScript and promise methods
Promise is a JavaScript object that represents the eventual result of an asynchronous operation, either success or failure.
Theory
TL;DR
- Analogy: ordering food delivery. You get a ticket (Promise) right away. Then food arrives (fulfilled) or you get a "sorry, out of stock" note (rejected). One answer, exactly once.
- Main difference from callbacks: a Promise is one object you chain with
.then(). Callbacks nest into each other, creating deeply indented, hard-to-follow code. - States are final: once a Promise settles (fulfilled or rejected), it never changes.
- Method rules:
Promise.all()when everything must succeed,Promise.allSettled()for partial success,Promise.race()for the fastest result,Promise.any()for the first success.
Quick example
// Simulates an API call
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => {
const ok = Math.random() > 0.5;
ok ? resolve("Data loaded") : reject("Network error");
}, 1000);
});
// Consuming the Promise
fetchData
.then(result => console.log(result)) // "Data loaded"
.catch(error => console.error(error)); // "Network error"After 1 second, exactly one branch fires. The Promise settles once and stays there.
Promise states
A Promise is always in one of three states:
- Pending: the async operation is still running, no result yet
- Fulfilled:
resolve(value)was called, operation succeeded - Rejected:
reject(error)was called, operation failed
Pending is the starting state. Fulfilled and rejected are terminal. Unlike browser events, a Promise fires exactly once and stays settled.
Key difference from callbacks
Callbacks nest. Every additional async step adds one more level of indentation, and error handling must happen at each level manually. A Promise chains: each .then() returns a new Promise, so you stay at one indentation level. Errors travel automatically to the nearest .catch(). That behavior removes callback hell for anything with more than one async step.
Promise methods
Promise.all()
Runs all promises in parallel and waits for every one to fulfill. Returns an array of results in the same order as input. If any single promise rejects, the whole thing rejects immediately with that error. The other promises keep running, but their results are ignored.
const [user, posts] = await Promise.all([
fetch('/api/user/1').then(r => r.json()),
fetch('/api/posts?userId=1').then(r => r.json())
]);
// Both requests fire at once; fails fast if either errorsUse this when all operations must succeed before continuing.
Promise.allSettled()
Same parallel execution as Promise.all(), but waits for every promise to finish regardless of outcome. Returns {status: 'fulfilled', value: ...} or {status: 'rejected', reason: ...} for each.
const results = await Promise.allSettled([fetchUser(), fetchPosts()]);
const loaded = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);Use this when partial success is acceptable, like a dashboard that renders whatever data actually loaded.
Promise.race()
Returns the first promise to settle, fulfilled or rejected. Everything else is discarded. The most common real use is a timeout wrapper:
const withTimeout = (promise, ms) =>
Promise.race([
promise,
new Promise((_, reject) => setTimeout(() => reject('Timeout'), ms))
]);If all inputs stay pending forever, Promise.race() stays pending forever too.
Promise.any()
Returns the first promise to fulfill. Skips rejections entirely. If all promises reject, it throws an AggregateError. Added in ES2021, so check if you need a polyfill for older targets.
// Use whichever CDN responds first
const resource = await Promise.any([
fetch('https://cdn1.example.com/lib.js'),
fetch('https://cdn2.example.com/lib.js'),
fetch('https://cdn3.example.com/lib.js')
]);How it works internally
When resolve() or reject() fires, the Promise marks itself settled and puts the .then() or .catch() handlers into the microtask queue. Microtasks run after the current synchronous code finishes but before the browser renders or the next setTimeout fires. That is why Promise.resolve().then(fn) always executes before setTimeout(fn, 0): different queues, different priority.
Common mistakes
1. Forgetting return inside .then() with curly braces
// Wrong - result is lost, next .then() gets undefined
fetch('/api/data')
.then(response => {
response.json(); // no return
})
.then(data => console.log(data)); // undefined
// Right
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data));Arrow functions with {} do not return implicitly.
2. No .catch() at the end of the chain
// Wrong - errors disappear, app continues in a broken state
fetch('/api').then(r => r.json()).then(showData);
// Right
fetch('/api').then(r => r.json()).then(showData).catch(console.error);3. Expecting Promise.all() to wait for everything before rejecting
In practice, this is the mistake I see most often in code review. Promise.all() bails on the first rejection. It does not wait for slower promises.
Promise.all([
Promise.resolve('OK'),
Promise.reject('BOOM'), // rejects immediately
slowPromise // its result is ignored
]).catch(err => console.log(err)); // "BOOM" instantlyUse Promise.allSettled() when you need every result.
4. Wrapping code that already returns a Promise
// Wrong - pointless overhead
function loadUser() {
return new Promise(resolve => resolve(fetch('/api/user')));
}
// Right - fetch already returns a Promise
function loadUser() {
return fetch('/api/user');
}Real-world usage
- React
useEffect:fetch('/api/data').then(r => r.json()).then(setData).catch(setError) - Express middleware:
Promise.all([db.query(...), cache.get(...)])for parallel data fetching - Redux Toolkit:
createAsyncThunkhandles the Promise lifecycle internally - Next.js
getServerSideProps:Promise.all([fetchUser(), fetchPosts()])for parallel SSR fetching
Follow-up questions
Q: What is the difference between Promise.all() and Promise.allSettled()?
A: Promise.all() rejects as soon as one input promise rejects. Promise.allSettled() waits for all promises to finish and returns a result object for each, containing status and either value or reason. No early exit.
Q: Why does Promise.resolve().then(fn) run before setTimeout(fn, 0)?
A: .then() callbacks go to the microtask queue. setTimeout goes to the macrotask queue. After each task, the engine drains all pending microtasks before picking the next macrotask. Microtasks always go first.
Q: What happens when you pass an empty array to Promise.all([])?
A: It resolves immediately with []. No promises to wait for.
Q: Can a Promise resolve with another Promise as its value?
A: Yes. If you call resolve(anotherPromise), the outer Promise adopts the state of the inner one and waits for it to settle. Any object with a .then() method (a "thenable") gets treated the same way.
Q: What does Promise.race() return if all inputs stay pending forever?
A: It stays pending forever. Nothing resolves or rejects it.
Examples
Loading user data with error handling
function loadUserProfile(userId) {
return fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) throw new Error('User not found');
return response.json();
})
.catch(error => {
console.error('Profile load failed:', error.message);
return null; // graceful fallback
});
}
loadUserProfile(1).then(profile => {
if (profile) renderProfile(profile);
});Throwing inside .then() is identical to calling reject(). The error goes straight to the nearest .catch().
Parallel dashboard requests
async function loadDashboard(userId) {
try {
const [user, posts, notifications] = await Promise.all([
fetch(`/api/users/${userId}`).then(r => r.json()),
fetch(`/api/posts?author=${userId}`).then(r => r.json()),
fetch(`/api/notifications/${userId}`).then(r => r.json())
]);
return { user, posts, notifications };
} catch (error) {
console.error('Dashboard failed:', error);
return null;
}
}Three requests fire at once. Total time equals the slowest one, not the sum of all three.
The Promise.all rejection edge case
const tasks = [
Promise.resolve('Fast'),
new Promise(resolve => setTimeout(() => resolve('Slow'), 100)),
Promise.reject('BOOM!'), // rejects immediately
new Promise(resolve => setTimeout(() => resolve('Never used'), 200))
];
Promise.all(tasks)
.then(results => console.log(results))
.catch(err => console.log('Failed:', err)); // "Failed: BOOM!" - no delayThe 200ms promise never delivers. Promise.all() already rejected. Switch to Promise.allSettled() and filter by status if you need all results.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.