Promise Polyfill (Promises/A+ Spec)

Hard100% Free~35 mins#promise-polyfill#promises-aplus#microtasks#queuemicrotask#state-machine#implementations
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';
4
5class MyPromise {
6 constructor(executor) {
7 this.state = PENDING;
8 this.value = undefined;
9 this.handlers = [];
10
11 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 };
20
21 const reject = (reason) => {
22 if (this.state !== PENDING) return;
23 this.state = REJECTED;
24 this.value = reason;
25 this.executeHandlers();
26 };
27
28 try {
29 executor(resolve, reject);
30 } catch (err) {
31 reject(err);
32 }
33 }
34
35 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 }
60
61 then(onFulfilled, onRejected) {
62 return new MyPromise((resolveNext, rejectNext) => {
63 this.handlers.push({ onFulfilled, onRejected, resolveNext, rejectNext });
64 this.executeHandlers();
65 });
66 }
67
68 catch(onRejected) {
69 return this.then(null, onRejected);
70 }
71}
72
73const logs = [];
74logs.push('sync-start');
75
76new 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 });
95
96logs.push('sync-end');
97
98queueMicrotask(() => {
99 queueMicrotask(() => {
100 queueMicrotask(() => {
101 queueMicrotask(() => {
102 console.log(logs.join(' '));
103 });
104 });
105 });
106});
Predict Console Output
Interactive Challenge

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.

Call Stack (Top = Active)
Global Execution Context
Lexical Scope / Bindings
logs:['sync-start']
Console Stream
> [empty]

Deep Technical Breakdown

Promises/A+ Specification Core Requirements

A Promise represents the eventual result of an asynchronous operation. To implement an educational polyfill:

  1. State Machine:
    • Three states: PENDING, FULFILLED, REJECTED.
    • Only transitions allowed: PENDING -> FULFILLED or PENDING -> REJECTED.
    • Immutability: Once settled, the state and value can never change.
  2. Asynchronous Execution (Microtasks):
    • Spec Requirement 2.2.4: onFulfilled and onRejected must not be called until the execution context stack contains only platform code.
    • We use queueMicrotask() to defer handler execution to the microtask queue.
  3. Promise Chaining (Returning a New Promise):
    • .then() must return a new MyPromise.
    • If onFulfilled returns value x, the new promise resolves with x.
    • If onFulfilled throws an exception e, the new promise rejects with e.
  4. Thenable Assimilation:
    • If resolve(x) is called with a value x that has a .then function (a thenable), the Promise adopts its state: x.then(resolve, reject).

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?