Skip to content

Promises Deep Dive: Chaining, Errors, all vs allSettled (Interview-Ready)

A practical guide to JavaScript Promises for interviews: states, chaining rules, error propagation, polyfills for all/allSettled, and concurrency patterns.

Rajesh DhimanFebruary 08, 2026~18 min
On this page

Promises are the backbone of modern JavaScript asynchronous architecture. Interviews focus heavily on whether you understand:

  • chaining mechanics and microtask scheduling
  • error propagation and unhandled rejection boundaries
  • implementing concurrency helpers (Promise.all, Promise.allSettled, Promise.race, Promise.any) from scratch

30‑second interview answer

A Promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It transitions deterministically from pending to either fulfilled or rejected. Calling .then() or .catch() always returns a new promise. Returning a primitive passes it down, returning a promise assimilates ("flattens") it, and throwing rejects down to the nearest .catch(). For concurrency: use Promise.all for fail-fast collective dependencies, allSettled when independent partial success is acceptable, race for timeouts, and any for redundant fallback mirrors.

Key points

  • .then() always creates and returns a brand-new Promise instance.
  • Errors bubble down the chained promises until caught by an onRejected handler or .catch().
  • .finally() executes without receiving arguments and does not alter the resolved value unless it throws.
  • An uncaught error inside an asynchronous timer callback (e.g. setTimeout) inside a Promise constructor will not trigger a Promise rejection—it escapes as a global uncaught exception.

1) Promise states and transitions (ECMA-262 Spec)

According to the ECMAScript specification (§27.2), an active Promise operates with internal slots [[PromiseState]] and [[PromiseResult]]:

  • pending: Initial state; neither fulfilled nor rejected.
  • fulfilled: Operation completed successfully ([[PromiseResult]] contains the resolved value).
  • rejected: Operation failed ([[PromiseResult]] contains the rejection reason/error).

Once a Promise moves from pending to either fulfilled or rejected, it is settled (or resolved). State transitions are irreversible. Subsequent calls to resolve() or reject() within the executor are silently ignored.


2) The chaining rule that matters

.then() returns a brand-new Promise

Promise.resolve(1)
  .then((x) => x + 1)
  .then((x) => console.log(x)); // 2
  1. Returning a value: The subsequent .then() immediately resolves with that value.
  2. Returning a Promise: The engine pauses propagation on this branch until the returned Promise settles, unwrapping its inner value (Promise resolution procedure).
Promise.resolve('user_123')
  .then((id) => fetch(`/api/users/${id}`))
  .then((res) => res.json())
  .then((user) => console.log(user.name));

3) Error propagation and the unhandled rejection trap

Errors propagate through the chain like an exception bubble until encountering an error handler:

Promise.resolve()
  .then(() => {
    throw new Error('Database connection failed');
  })
  .then(() => {
    console.log('This will NEVER execute');
  })
  .catch((err) => {
    console.log('Handled gracefully:', err.message);
    return 'fallback_cache'; // Recovered! Subsequent .then receives this value.
  })
  .then((val) => {
    console.log('Chain continues with:', val); // "Chain continues with: fallback_cache"
  });

The Async Executor Exception Trap (Senior Interview Favorite)

Interviewers frequently test your understanding of synchronous executor errors vs asynchronous callback errors:

// Scenario A: Synchronous throw inside executor
new Promise((resolve, reject) => {
  throw new Error('Sync boom');
}).catch((err) => console.log('Caught safely:', err.message));
// ✅ Caught safely: Sync boom (constructor wraps executor in implicit try/catch)
 
// Scenario B: Asynchronous throw inside executor callback
new Promise((resolve, reject) => {
  setTimeout(() => {
    throw new Error('Async boom'); // ❌ Bypasses Promise error boundary!
  }, 100);
}).catch((err) => console.log('Will this catch?'));
// ❌ DOES NOT CATCH! Uncaught Error: Async boom.
// The stack trace belongs to the timer tick, not the Promise executor.
// Fix: Always call reject(err) inside asynchronous callbacks.

Global Unhandled Rejections

When a promise rejects without a .catch() attached in the current microtask turn:

  • Browser: Fires window.addEventListener('unhandledrejection', event => ...)
  • Node.js: Emits process.on('unhandledRejection', (reason, promise) => ...) (terminates process in Node 15+ if unhandled).

4) .finally() behavior and semantics

.finally(callback) guarantees execution regardless of outcome (analogous to try...finally in synchronous code).

  • It receives no arguments (callback takes 0 parameters).
  • It returns a new Promise that settles with the original value or reason of the previous Promise.
  • Exception: If the callback inside .finally() throws or returns a rejected promise, the chain rejects with that new error, overriding the previous result.
Promise.resolve('auth_token')
  .finally(() => {
    hideSpinner(); // Executes for cleanup
    return 'tampered_value'; // Ignored!
  })
  .then((val) => {
    console.log(val); // 'auth_token' (original value preserved)
  });

5) Concurrency helpers: Whiteboard implementations

Interviewers often ask candidates to implement Promise.all or Promise.allSettled from scratch. Here are battle-tested, interview-ready implementations.

Machine-Coding Polyfill: Promise.all

Requirements:

  1. Accepts any iterable (arrays, Sets).
  2. Non-promise items must be wrapped with Promise.resolve().
  3. If input is empty, resolves immediately with an empty array [].
  4. Maintains original item order in results, regardless of which finishes first.
  5. Fails immediately on first rejection (fail-fast).
export function promiseAll(iterable) {
  return new Promise((resolve, reject) => {
    const items = Array.from(iterable);
    const total = items.length;
 
    if (total === 0) {
      resolve([]);
      return;
    }
 
    const results = new Array(total);
    let completedCount = 0;
 
    items.forEach((item, index) => {
      // Promise.resolve wraps raw values or handles foreign thenables
      Promise.resolve(item)
        .then((value) => {
          results[index] = value; // Preserve original index position
          completedCount += 1;
 
          if (completedCount === total) {
            resolve(results);
          }
        })
        .catch((error) => {
          // Reject immediately on first encountered error
          reject(error);
        });
    });
  });
}

Machine-Coding Polyfill: Promise.allSettled

Requirements:

  1. Never rejects. Always resolves with an array of objects.
  2. Fulfilled items: { status: 'fulfilled', value: <val> }.
  3. Rejected items: { status: 'rejected', reason: <err> }.
export function promiseAllSettled(iterable) {
  return new Promise((resolve) => {
    const items = Array.from(iterable);
    const total = items.length;
 
    if (total === 0) {
      resolve([]);
      return;
    }
 
    const results = new Array(total);
    let completedCount = 0;
 
    items.forEach((item, index) => {
      Promise.resolve(item)
        .then((value) => {
          results[index] = { status: 'fulfilled', value };
        })
        .catch((reason) => {
          results[index] = { status: 'rejected', reason };
        })
        .finally(() => {
          completedCount += 1;
          if (completedCount === total) {
            resolve(results);
          }
        });
    });
  });
}

6) Helper comparison quick reference

Helper Resolves When Rejects When Common Real-World Use Case
Promise.all ALL promises fulfill ANY single promise rejects Fetching page config + user profile simultaneously (fail fast).
Promise.allSettled ALL promises settle (never rejects) Never Uploading multiple independent files (show which passed and failed).
Promise.race FIRST promise settles (resolve OR reject) FIRST promise rejects Request timeout race (Promise.race([fetch(url), timeout(5000)])).
Promise.any FIRST promise fulfills ALL promises reject (AggregateError) Fetching asset from redundant CDNs/mirrors (first working one wins).

7) Interview Execution Trace Puzzle

Question: What is printed to the console in what exact sequence?

console.log('1');
 
Promise.resolve()
  .then(() => {
    console.log('2');
    throw new Error('Err');
  })
  .catch((e) => {
    console.log('3');
    return 'recovered';
  })
  .finally(() => {
    console.log('4');
  })
  .then((res) => {
    console.log('5:', res);
  });
 
console.log('6');

Trace:

  1. Synchronous execution runs first: console.log('1') prints 1.
  2. Promise.resolve() queues the first .then() into the Microtask Queue.
  3. Synchronous execution continues: console.log('6') prints 6.
  4. Call stack is empty; Event Loop processes microtasks:
    • First microtask runs: prints 2, throws error.
    • Rejection passes to .catch: prints 3, returns 'recovered'.
    • Chain proceeds to .finally: prints 4.
    • Chain proceeds to final .then: prints 5: recovered.

Final output sequence: 1, 6, 2, 3, 4, 5: recovered.


Summary checklist

  • I can describe the 3 internal Promise states (pending, fulfilled, rejected).
  • I understand that .then() returns a new promise and how values vs promises are unwrapped.
  • I know why errors inside asynchronous callbacks in an executor cannot be caught by .catch().
  • I can write Promise.all and Promise.allSettled from memory without bugs.
  • I can explain the differences between all, allSettled, race, and any.

Authoritative Standards & References


Written by Rajesh Dhiman at Eunix Tech for the Frontend Interview preparation track.

Companion Book

Looking for structured, offline interview prep?

Free online lessons • Portable Amazon Kindle / Paperback

JavaScript Interview Guide cover
Recommended for Core JS Rounds

JavaScript Interview Guide

By Rajesh Dhiman

Deep dive into the Event Loop, microtask draining, closures, prototypal inheritance, and asynchronous execution patterns.

View on Amazon ↗Kindle & Paperback
Frontend Interview Guide (2025 Edition) cover
Recommended for React & Frontend Rounds

Frontend Interview Guide (2025 Edition)

By Rajesh Dhiman

Comprehensive preparation covering React internals, Web Performance, CSS architecture, and system design questions.

View on Amazon ↗Kindle & Paperback

Note: All lessons on Frontend Interview remain completely free to read online. As an Amazon Associate, we may earn from qualifying purchases.

RD

Written by Rajesh Dhiman

Authored by Rajesh Dhiman and engineered at Eunix Tech. Focused on deep-dive frontend engineering, JavaScript runtime internals, React architecture, and web performance.