Build: Promise Polyfills
Introduction
Promise polyfills are a classic advanced JavaScript machine-coding challenge. The interviewer is not looking for the entire ECMAScript spec; they want the state machine, asynchronous handler scheduling, thenable assimilation, chaining, rejection propagation, and Promise.all behavior.
This topic builds a compact MyPromise with then, catch, resolve, reject, and all.
Why This Matters
Promises are the foundation of modern async JavaScript. Reimplementing them forces you to explain why callbacks run asynchronously, how returned values become chained promises, and how Promise.all preserves order while resolving concurrently.
Theory
Promise state machine
A promise starts as pending. It can transition exactly once to fulfilled with a value or rejected with a reason. After settlement, the state and value never change.
Handler queue
then returns a new promise. If the current promise is pending, store the handler. If it is settled, schedule the handler asynchronously. The returned promise resolves with the callback result; if the callback throws, it rejects.
Thenable assimilation
If resolve receives another promise-like object with a then method, adopt that object's eventual state. This is what makes return fetch(...) or return anotherPromise inside then flatten instead of creating nested promises.
Promise.all
all starts all inputs, stores each result by original index, decrements a remaining counter, resolves when all complete, and rejects immediately on the first rejection.
Visual Diagrams
pending | +-- resolve(value) --> fulfilled(value) | +-- reject(reason) --> rejected(reason) settled states are final then callbacks create a new chained promise
The one-way state transition is the core invariant.
Code Examples
Complete MyPromise and MyPromise.all
This is intentionally compact, but it includes the interview-critical pieces: async scheduling, chaining, thenable adoption, and order-preserving all.
Playground
Press Run to execute the code and see output here.
Output Prediction
Predict the output #1
1function asyncRun(fn) {2 if (typeof queueMicrotask === 'function') {3 queueMicrotask(fn);4 } else {5 setTimeout(fn, 0);6 }7}8 9function MyPromise(executor) {10 this._state = 'pending';11 this._value = undefined;12 this._handlers = [];13 var promise = this;14 executor(function (value) {15 settle(promise, 'fulfilled', value);16 }, function (reason) {17 settle(promise, 'rejected', reason);18 });19}20 21function settle(promise, state, value) {22 if (promise._state !== 'pending') {23 return;24 }25 26 promise._state = state;27 promise._value = value;28 asyncRun(function () {29 promise._handlers.forEach(function (handler) {30 handler(value);31 });32 });33}34 35MyPromise.prototype.then = function (onFulfilled) {36 var current = this;37 return new MyPromise(function (resolve) {38 function run(value) {39 resolve(onFulfilled(value));40 }41 42 if (current._state === 'pending') {43 current._handlers.push(run);44 } else {45 asyncRun(function () {46 run(current._value);47 });48 }49 });50};51 52MyPromise.resolve = function (value) {53 return new MyPromise(function (resolve) {54 resolve(value);55 });56};57 58MyPromise.resolve(2)59 .then(function (value) {60 return value + 3;61 })62 .then(function (value) {63 console.log(value);64 });65 66console.log('sync');Coding Exercises
Implement MyPromise with then, catch, and all
HardImplement a compact MyPromise.
Requirements:
- Constructor accepts an executor
(resolve, reject). - State can move from pending to fulfilled or rejected exactly once.
then(onFulfilled, onRejected)returns a new chained promise.catch(onRejected)works asthen(null, onRejected).- Handlers run asynchronously.
- Resolving with a thenable adopts that thenable.
MyPromise.resolve,MyPromise.reject, andMyPromise.allwork.MyPromise.allpreserves input order and rejects on first rejection.
Constraints:
- Do not call native
Promisefor the state machine. queueMicrotaskorsetTimeoutmay be used only for scheduling.
Interview Questions
1Why must promise callbacks run asynchronously?
Asynchronous scheduling makes behavior consistent whether a promise settles before or after then is attached. It also prevents surprising reentrancy where callbacks run in the middle of the current call stack.
Follow-ups
- What is the difference between microtasks and macrotasks?
- Where do promise callbacks sit in the event loop?
2How does `Promise.all` preserve order?
It stores each fulfilled value at the original input index. Promises may resolve in any order, but the final array is ordered by input position, not completion time.
Quiz
1. What should happen if a `then` callback throws?
Summary
- A promise is a one-way pending to fulfilled/rejected state machine.
- `then` returns a new promise and schedules callbacks asynchronously.
- Resolving with a thenable adopts that thenable's eventual state.
- `Promise.all` preserves input order and rejects on first rejection.
Cheat Sheet
Promise polyfill checklist
- State: pending, fulfilled, rejected.
- Store value/reason after settlement.
- Queue handlers while pending.
- Schedule handlers asynchronously.
thenreturns a new promise.- Callback return value resolves the chained promise.
- Thrown callback error rejects the chained promise.
- Assimilate thenables.
all: index results, remaining counter, first rejection wins.