Skip to Content
Course content

194: Building a Simple Promise Implementation from Scratch

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed a recurring theme when talking to developers about Promises: many of you treat them as if they are the cause of asynchronicity. You might think that wrapping a function in a new Promise() is what actually makes the code run in the background. It's a common mental shortcut, but it's fundamentally wrong.

Promises aren't async magic; they are state machines

If you write new Promise(resolve => { console.log('Hi'); resolve(); }), that 'Hi' prints immediately. There is no "magic thread" being spawned. The Promise itself doesn't make things asynchronous; it just provides a standardized way to track the result of an asynchronous operation that is already happening elsewhere (like a fetch call or a setTimeout).

Think of a Promise as a receipt at a deli. The receipt isn't the sandwich; it's just a piece of paper that says, "I'm currently Pending. Eventually, I will either be Fulfilled (here is your sandwich) or Rejected (we ran out of rye bread)." If you try to eat the receipt, you're not getting any lunch.

Building the internal state tracker

To build our own version, we need a class that tracks three things: the current state, the resulting value, and a list of functions to call once the state changes. I'll call our implementation MyPromise.

class MyPromise {
  constructor(executor) {
    this.state = 'PENDING';
    this.value = undefined;
    this.handlers = [];

    // We define resolve and reject inside the constructor 
    // so they have access to this specific instance.
    const resolve = (value) => this._transition('FULFILLED', value);
    const reject = (error) => this._transition('REJECTED', error);

    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }

  _transition(state, value) {
    if (this.state !== 'PENDING') return; // Promises are immutable once settled

    this.state = state;
    this.value = value;

    // Once settled, we fire off all the queued .then() callbacks
    this.handlers.forEach(handler => handler());
  }
}

Notice that _transition is where the "truth" of the Promise lives. I added a check to ensure that if a Promise is already fulfilled, it can't suddenly become rejected. That's a core part of the spec.

Handling the .then() queue

The trickiest part is .then(). If the Promise is already finished, the callback should run immediately. If it's still pending, we need to store that callback in our handlers array and wait.

In a real JS engine, .then() callbacks are pushed to the Microtask Queue, meaning they run after the current script finishes but before the browser repaints. For our simple version, we'll use queueMicrotask to mimic this behavior so our Promise doesn't behave synchronously.

  then(onFulfilled) {
    return new MyPromise((resolve) => {
      const executeHandler = () => {
        queueMicrotask(() => {
          if (this.state === 'FULFILLED') {
            const result = onFulfilled(this.value);
            resolve(result);
          } else {
            // For simplicity, we are ignoring rejection handling here
            resolve(); 
          }
        });
      };

      if (this.state === 'FULFILLED') {
        executeHandler();
      } else {
        this.handlers.push(executeHandler);
      }
    });
  }

I've returned a new MyPromise inside the then method. This is why you can chain Promises. Each .then() creates a new "receipt" that depends on the previous one being fulfilled.

Now, let's test it with something that is actually asynchronous, like a timer. If we wrap a setTimeout in our MyPromise, we can see the state machine in action: it starts pending, the timer expires, resolve() is called, the state transitions to fulfilled, and the handlers are flushed.

const p = new MyPromise((resolve) => {
  setTimeout(() => resolve("Sandwich is ready!"), 1000);
});

p.then(val => console.log(val)); 
console.log("I'm waiting..."); 
// Output:
// "I'm waiting..."
// (1 second pause)
// "Sandwich is ready!"



📋 Practical Task

Implement a Promise.all() Clone for MyPromise

Now that you have the MyPromise class working, your task is to create a static method called all(). This method should take an array of MyPromise instances and return a single MyPromise that resolves only when all the input promises have resolved.

Requirements:

  • The returned promise should resolve to an array of values in the same order as the input array.
  • If any of the input promises reject, the main promise should reject immediately.
  • Do not use the built-in Promise.all(); use your MyPromise implementation.
// Expected usage:
MyPromise.all([
  new MyPromise(res => setTimeout(() => res(1), 100)),
  new MyPromise(res => setTimeout(() => res(2), 200)),
  new MyPromise(res => setTimeout(() => res(3), 50))
]).then(results => {
  console.log(results); // Should log [1, 2, 3] after 200ms
});
Rating
0 0

There are no comments for now.

to be the first to leave a comment.