Promises are the foundation of modern async JavaScript. Every fetch call, every async function, every await — they all depend on Promises. Building one from scratch reveals exactly how then, catch, finally, and chaining work at the microtask level.
Prerequisites: Async #1 — The Event Loop
1. The Promise State Machine
A Promise is always in one of three states:
- Pending — initial state, waiting for resolution
- Fulfilled — operation succeeded, a value is available
- Rejected — operation failed, a reason is available
Once fulfilled or rejected, a Promise is settled — it can never change state again.
Loading editor...
2. Building MyPromise — Constructor
Loading editor...
3. Implementing .then() and .catch()
.then() registers callbacks and returns a NEW promise for chaining. Crucially, .then() callbacks run asynchronously as microtasks — they're scheduled via queueMicrotask(), which means they execute after the current synchronous code finishes but before any macrotasks (like setTimeout):
Synchronous code → drain all microtasks → one macrotask → drain all microtasks → ...
This is why Promise.resolve().then(fn) runs fn after the current call stack empties.
Loading editor...
4. .finally() — Runs Regardless
finally always runs but doesn't receive the value/reason, and passes through the original resolution:
Loading editor...
5. Static Methods — MyPromise.resolve() and MyPromise.reject()
Loading editor...
6. Chaining and the Flattening Trick
The key to promise chaining: when onFulfilled returns a promise, the .then() wrapper waits for that promise. Our resolve(value) in the constructor already handles thenables — so resolve(result) in the fulfillTask auto-flattens:
Loading editor...
7. Full Minimal Implementation
Here's the complete self-contained implementation for reference:
Loading editor...
Key Takeaways
- A Promise is a state machine: pending → fulfilled (value) or pending → rejected (reason).
.then()returns a new Promise — this is what enables chaining.- Callbacks run via microtasks (
queueMicrotask), not synchronously. - Resolve-thenable flattening: if
resolve(value)gets a thenable, the promise adopts that thenable's state. .catch()recovers: ifonRejecteddoesn't throw, the chain continues as fulfilled.constructor,then,resolve,reject,queueMicrotask— these are the five pieces you need.
