Async #3 — Building a Promise from Scratch

Implement the Promise constructor, then, catch, finally, resolve, reject — with state machine (pending/fulfilled/rejected), chaining, and microtask scheduling.

14 min read
JavaScript
Async
Promise
Polyfill

TABLE OF CONTENTS
Async #3 — Building a Promise from Scratch

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: if onRejected doesn't throw, the chain continues as fulfilled.
  • constructor, then, resolve, reject, queueMicrotask — these are the five pieces you need.


Let's Connect

© 2026 Naveen Karthik // Built with React & MUI