Skip to main content

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

javascript
// 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.

javascript
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 errors

Use 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.

javascript
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:

javascript
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.

javascript
// 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

javascript
// 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

javascript
// 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.

javascript
Promise.all([ Promise.resolve('OK'), Promise.reject('BOOM'), // rejects immediately slowPromise // its result is ignored ]).catch(err => console.log(err)); // "BOOM" instantly

Use Promise.allSettled() when you need every result.

4. Wrapping code that already returns a Promise

javascript
// 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: createAsyncThunk handles 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

javascript
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

javascript
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

javascript
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 delay

The 200ms promise never delivers. Promise.all() already rejected. Switch to Promise.allSettled() and filter by status if you need all results.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?