Promise Polyfill (Promises/A+ Spec)
Key Learning Objectives
Model the 3 core states of a Promise: PENDING, FULFILLED, and REJECTED.
Understand first-settlement-wins semantics: once a Promise transitions from PENDING, subsequent resolve or reject calls are permanently ignored.
Schedule .then and .catch reaction callbacks asynchronously on the microtask queue using queueMicrotask.
Implement Promise chaining: each call to .then returns a new Promise resolving to the callback return value.
Handle exception propagation: errors thrown inside callbacks automatically reject the chained promise.
The Interview Problem
What is logged to the console when the following educational MyPromise implementation executes with chaining, error throwing, and catch recovery?
1const PENDING = 'PENDING';2const FULFILLED = 'FULFILLED';3const REJECTED = 'REJECTED';45class MyPromise {6 constructor(executor) {7 this.state = PENDING;8 this.value = undefined;9 this.handlers = [];1011 const resolve = (val) => {12 if (this.state !== PENDING) return;13 if (val && typeof val.then === 'function') {14 return val.then(resolve, reject);15 }16 this.state = FULFILLED;17 this.value = val;18 this.executeHandlers();19 };2021 const reject = (reason) => {22 if (this.state !== PENDING) return;23 this.state = REJECTED;24 this.value = reason;25 this.executeHandlers();26 };2728 try {29 executor(resolve, reject);30 } catch (err) {31 reject(err);32 }33 }3435 executeHandlers() {36 if (this.state === PENDING) return;37 queueMicrotask(() => {38 for (const { onFulfilled, onRejected, resolveNext, rejectNext } of this.handlers) {39 try {40 if (this.state === FULFILLED) {41 if (typeof onFulfilled === 'function') {42 resolveNext(onFulfilled(this.value));43 } else {44 resolveNext(this.value);45 }46 } else if (this.state === REJECTED) {47 if (typeof onRejected === 'function') {48 resolveNext(onRejected(this.value));49 } else {50 rejectNext(this.value);51 }52 }53 } catch (err) {54 rejectNext(err);55 }56 }57 this.handlers = [];58 });59 }6061 then(onFulfilled, onRejected) {62 return new MyPromise((resolveNext, rejectNext) => {63 this.handlers.push({ onFulfilled, onRejected, resolveNext, rejectNext });64 this.executeHandlers();65 });66 }6768 catch(onRejected) {69 return this.then(null, onRejected);70 }71}7273const logs = [];74logs.push('sync-start');7576new MyPromise((res, rej) => {77 res(10);78 rej('ignored');79})80 .then((val) => {81 logs.push('val:' + val);82 return val * 2;83 })84 .then((val) => {85 logs.push('val:' + val);86 throw new Error('boom');87 })88 .catch((err) => {89 logs.push('caught:' + err.message);90 return 'recovered';91 })92 .then((val) => {93 logs.push('final:' + val);94 });9596logs.push('sync-end');9798queueMicrotask(() => {99 queueMicrotask(() => {100 queueMicrotask(() => {101 queueMicrotask(() => {102 console.log(logs.join(' '));103 });104 });105 });106});
Predict Console Output
Select the option that matches what standard ECMAScript prints to the console:
sync-start sync-end val:10 val:20 caught:boom final:recovered
sync-start val:10 val:20 caught:boom final:recovered sync-end
sync-start sync-end val:10 val:20 caught:ignored final:recovered
sync-start sync-end val:10 val:20 Uncaught Error: boom
V8 Engine Execution Trace
Step 1 of 7 (Line 63)logs.push('sync-start') runs synchronously. Script execution begins.
Deep Technical Breakdown
Promises/A+ Specification Core Requirements
A Promise represents the eventual result of an asynchronous operation. To implement an educational polyfill:
- State Machine:
- Three states:
PENDING,FULFILLED,REJECTED. - Only transitions allowed:
PENDING -> FULFILLEDorPENDING -> REJECTED. - Immutability: Once settled, the state and value can never change.
- Three states:
- Asynchronous Execution (Microtasks):
- Spec Requirement 2.2.4:
onFulfilledandonRejectedmust not be called until the execution context stack contains only platform code. - We use
queueMicrotask()to defer handler execution to the microtask queue.
- Spec Requirement 2.2.4:
- Promise Chaining (Returning a New Promise):
.then()must return a newMyPromise.- If
onFulfilledreturns valuex, the new promise resolves withx. - If
onFulfilledthrows an exceptione, the new promise rejects withe.
- Thenable Assimilation:
- If
resolve(x)is called with a valuexthat has a.thenfunction (a thenable), the Promise adopts its state:x.then(resolve, reject).
- If
Polyfill Scope vs Full Spec
This educational implementation covers core state transitions, chaining, error recovery, and microtasks. A production 100% compliant Promises/A+ polyfill requires additional safeguards: cycle detection (x === promise2), non-function handler pass-through, and defensive single-invocation flags.
Common Traps & Mistakes
Executing `.then` callbacks synchronously when the promise is already fulfilled, violating Promises/A+ section 2.2.4 and causing race conditions.
Returning `this` from `.then()` instead of instantiating a new Promise, making independent chaining and branch recovery impossible.
Failing to catch exceptions thrown inside the executor function or reaction callbacks, causing unhandled errors to bypass promise rejection.
Allowing multiple settlements: failing to guard against calling `resolve()` followed by `reject()`.
FAANG Follow-Up Probes
Probe #1
How does the Promise Resolution Procedure detect and reject circular promise chains (when a promise returns itself)?
Probe #2
How would you implement static `MyPromise.resolve(val)` and `MyPromise.reject(reason)` methods?
Probe #3
Why does Promises/A+ specify microtask scheduling over macrotask scheduling (setTimeout) for callback dispatch?
