Ad – 728Γ—90
πŸš€ Advanced JS

JavaScript Promises – Handling Async Operations Cleanly

A Promise is a JavaScript object that represents the eventual result (or failure) of an asynchronous operation. Introduced in ES6, Promises solved the callback hell problem by enabling chainable, composable async code. This lesson covers the three states of a Promise, how to chain .then() handlers, handle errors with .catch(), run async operations in parallel with Promise.all(), and understand where Promises sit in the JavaScript event loop via the microtask queue.

⏱️ 22 min read 🎯 Advanced πŸ“… Updated 2026

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.

StateMeaningTriggered byCan transition to
PendingInitial state β€” not yet settledCreation via new Promise()Fulfilled or Rejected
FulfilledOperation completed successfullyCalling resolve(value)Never changes again
RejectedOperation failedCalling reject(reason) or throwing inside executorNever changes again
JavaScript
// 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...');
β–Ά Output
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.

JavaScript
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
  });
β–Ά Output
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).

JavaScript
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');
  });
β–Ά Output
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:

JavaScript
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));
β–Ά Output
all: ['slow', 'fast']
race winner: fast
any winner: fast
settled: fulfilled slow
settled: fulfilled fast
settled: rejected  boom
πŸ’‘
Choosing the right combinator

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

JavaScript
// 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
β–Ά Output
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.

JavaScript
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
β–Ά Output
1: synchronous start
2: synchronous end
3: first microtask
3b: second microtask
4: setTimeout (macrotask)
⚠️
Unhandled promise rejections

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.

Ad – 336Γ—280

πŸ‹οΈ Practical Exercise

Build a retry(fn, maxAttempts, delayMs) utility that:

  • Calls the async function fn() (returns a Promise).
  • If it rejects, waits delayMs milliseconds and retries.
  • Retries up to maxAttempts times 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

Can I resolve a Promise with another Promise? +

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.

What happens if I throw inside a .then() callback? +

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.

What is the difference between Promise.any and Promise.race? +

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