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.
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- Returning a value: The subsequent
.then()immediately resolves with that value. - 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 (
callbacktakes 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:
- Accepts any iterable (arrays, Sets).
- Non-promise items must be wrapped with
Promise.resolve(). - If input is empty, resolves immediately with an empty array
[]. - Maintains original item order in results, regardless of which finishes first.
- 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:
- Never rejects. Always resolves with an array of objects.
- Fulfilled items:
{ status: 'fulfilled', value: <val> }. - 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:
- Synchronous execution runs first:
console.log('1')prints1. Promise.resolve()queues the first.then()into the Microtask Queue.- Synchronous execution continues:
console.log('6')prints6. - Call stack is empty; Event Loop processes microtasks:
- First microtask runs: prints
2, throws error. - Rejection passes to
.catch: prints3, returns'recovered'. - Chain proceeds to
.finally: prints4. - Chain proceeds to final
.then: prints5: recovered.
- First microtask runs: prints
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.allandPromise.allSettledfrom memory without bugs. - I can explain the differences between
all,allSettled,race, andany.
Authoritative Standards & References
- ECMAScript® 2026 Language Specification: Promise Objects (§27.2)
- MDN Web Docs: Promise
- MDN Web Docs: Using Promises Guide
Written by Rajesh Dhiman at Eunix Tech for the Frontend Interview preparation track.
Looking for structured, offline interview prep?
Free online lessons • Portable Amazon Kindle / Paperback
JavaScript Interview Guide
By Rajesh Dhiman
Deep dive into the Event Loop, microtask draining, closures, prototypal inheritance, and asynchronous execution patterns.
Frontend Interview Guide (2025 Edition)
By Rajesh Dhiman
Comprehensive preparation covering React internals, Web Performance, CSS architecture, and system design questions.
Note: All lessons on Frontend Interview remain completely free to read online. As an Amazon Associate, we may earn from qualifying purchases.
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.