Implementing promises. Ish.

Promises can be hard to get a grip on. While I’ve used them quite a bit myself, I’ve never stopped to consider how to implement them – that is, until now.

Disclaimer: To be completely honest, the main goal of this article is for me to properly teach myself “the magic” behind promises. You are, however, welcome to come along for the ride!

I’ll be implementing a subset of the A+ promise spec, that is, enough to have the following run as expected:

Promise.resolve("Never gonna give you up")
  .then(console.log)
  .then(() => { throw new Error("Never gonna let you down") })
  .then(() => console.log("Never gonna print this sentence"))
  .catch(error => console.log(error.message))
  .then(() => {
    console.log("Never gonna run around");
    return Promise.reject(new Error("And desert you"));
  })
  .catch(error => console.log(error.message));

You’ll never guess what the output will be.

Let’s get started #

We’ll split the implementation in four parts:

  1. Constructor and top-level resolvers
  2. A few utility functions
  3. Attaching handlers with done
  4. Running through the promise chain with then and catch

Constructor and top-level resolvers #

Ok, time to mention terminology real quickly:

That out of the way, let’s set up the constructor:

const PENDING = 0, FULFILLED = 1, FAILED = 2;

class Promise {
  constructor(fn) {
    this.state = PENDING;

    const fulfill = value => {
      this.state = FULFILLED;
      this.value = value;
      this.fulfillmentHandlers.forEach(handler => (
        handler(this.value)
      ));
    };

    const reject = value => {
      this.state = FAILED;
      this.value = value;
      this.rejectionHandlers.forEach(handler => (
        handler(this.value)
      ));
    };

    const resolve = valueOrPromise => {
      if (!Promise.isPromise(valueOrPromise)) {
        return fulfill(valueOrPromise);
      }
      return valueOrPromise.then(resolve, reject);
    };

    fn(resolve, reject);
  }

  // ...
}

Boom! And we’re off.

As expected, the constructor takes a function, and at one point this function is called with a pair of functions for resolving and rejecting the promise, respectively. Not that exciting, I agree.

More interesting is what goes on in between. First, let’s talk about fulfill and reject, as they’re pretty similar. They both take a value, and set it as an instance variable on the promise object itself, then move on to call any registered handlers to spread the news and get the chain started.

However, fulfill isn’t the one going into the callback function – it’s wrapped in the resolve function. The reason for this is that the resolve function needs to be able to handle returned promises of the following slightly contrived form:

new Promise(resolve => resolve(Promise.reject(new Error("oh no"))));

Therefore, if we have a promise on our hands we need to be a bit clever: Instead of assuming we’re supposed to be resolving, delegate that job to the then method of the given promise.

For how to implement then, keep reading. That’s the good part.

Utility functions #

Let’s add some utility functions while we’re at it:

// A slightly naïve test for whether an object is a Promise
Promise.isPromise = maybePromise => (
  maybePromise && typeof maybePromise.then === "function"
);

Promise.resolve = value => (
  new Promise(resolve => resolve(value))
);
Promise.reject = value => (
  new Promise((resolve, reject) => reject(value))
);

Nothing remarkable here, apart from this making it obvious how simple the typical Promise.resolve and Promise.reject utility functions really are.

Attaching handlers #

Let’s start by implementing done, which is a bit simpler than then and catch, and which we will reuse later.

class Promise {
  constructor(fn) {
    this.fulfillmentHandlers = [];
    this.rejectionHandlers = [];

    // ...
  }

  done(onFulfilled, onRejected) {
    if (this.state === PENDING) {
      this.fulfillmentHandlers.push(onFulfilled);
      this.rejectionHandlers.push(onRejected);
    } else {
      if (this.state === FULFILLED &&
          typeof onFulfilled === "function") {
        setTimeout(() => onFulfilled(this.value));
      }
      if (this.state === REJECTED &&
          typeof onRejected === "function") {
        setTimeout(() => onRejected(this.value));
      }
    }
  }
}

As you can see, done is fairly simple: “If we are a pending promise, keep track of the callbacks – or potentially, if we’ve already settled for a value, call them.”

The setTimeouts are there to ensure that we’re always resolving the promise in an asynchronous manner. It is expected of us.

Running through the promise chain #

Now, let’s reuse done to implement then and catch. Now, as catch can be implemented as handler => then(null, handler), let’s talk about then from now on.

A key feature of then is that it returns a new promise, while done doesn’t really care what happens next.

Let’s have a look at my naïve implementation.

class Promise {
  // ...

  then(onFulfilled, onRejected) {
    return new Promise((resolve, reject) => {
      const handleFulfillment = value => (
        if (typeof onFulfilled !== "function") {
          return resolve(value);
        }

        try {
          return resolve(onFulfilled(value));
        } catch (ex) {
          return reject(ex);
        }
      );

      const handleRejection = value => (
        if (typeof onRejected !== "function") {
          return reject(value);
        }

        try {
          return resolve(onRejected(value));
        } catch (ex) {
          return reject(ex);
        }
      );

      return this.done(handleFulfillment, handleRejection);
    });
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }
}

Now, that’s a bit harder to wrap one’s head around. So then returns a new Promise of some kind – then what?

The key point is that the code ends up calling done with two arguments. This call loosely translates to “what to do when the current promise is fulfilled or rejected?”

Well, we want to do one of two things:

  1. If the current promise is fulfilled, call the fulfillment handler that was passed in. If that function doesn’t throw or it just isn’t specified, resolve the returned promise too.
  2. If the current promise is rejected, call the rejection handler that was passed in. If it’s there and doesn’t throw, fulfill the returned promise.

Otherwise, reject.

Actually performing these calls is done either by done directly, or by fulfill or reject in the constructor.

Conclusion #

Aaaand that’s it, really. We’ve implemented promises! Hope you learned something along the way – I certainly did.

The most important take-away for me was:

For the full implementation, check out the following repository:
https://github.com/myrlund/promisish/blob/master/promise.js

 
10
Kudos
 
10
Kudos