Implementing a simple Promise in Javascript

Zhi Sun
The Startup
Published in
6 min readJan 18, 2021
Photo By Zhi Sun (Reflection Lake, Mt.Rainier)

In front-end engineering interviews and daily front-end development, we encounter Promises all the time. In these interviews, I have been asked to implement a Promise from scratch, implement Promise.all(), write a function to limit maximum concurrent Promises and multiple questions about code sequencing around Promises. In daily front-end development, we use Promises to fetch data and ensure our code runs in the correct sequence.

When asked about Promises in interviews I always get a little afraid. I know how to use Promises and some basic rules but if things get fancy I fear I will fail. I searched online and read several implementations of Promises. They are pretty complicated and I could not figure out why they do certain things. Therefore, I decided to write this article, to implement a simple Promise, one step at a time. Hopefully you can benefit from my journey of understanding.

This is not a comprehensive reimplementation of the PromiseA+ standard Promise; just my little practice to better understand it. If you feel a little fuzzy about what Promises are and how they are used, I recommend you take a few minutes to read Promise on MDN.

Let’s get started! We will start with how to create and consume a Promise. Then, we will implement a basic version and iterate on it to support async and chaining.

How to create a Promise?

Promises ensure our code runs in the correct sequence. There is basically one way to create a Promise but two ways to use it.

  1. Create a Promise and resolve it immediately.

Running the following will create a Promise that resolves it immediately.

Note: setTimeout simulates a long running asynchronous process such as calling a backend service.

2. Create a function that returns a Promise and resolve when calling the function.

This looks simple but it was my first “aha!” moment. A new Promise will immediately execute. If we do not want execute immediately, we need to put it into a function and call it when desired.

Now we know the first thing we want to do: create a constructor which takes one function with two params (resolve, reject). They are functions the caller uses to settle their promises.

How to consume a Promise?

After a Promise is created, it can be consumed. There are three main ways to consume a Promise: then(), catch(), finally().

.then(res => onFulfilled(res), err => onRejected(err)) — takes two functions defined by the consumer. They are called after a Promise is resolved or rejected.

.catch(err => onRejected(err)) — takes a function which is executed when the Promise is rejected.

.finally(() => onSettled()) — takes a function when the Promise is either resolved or rejected. It is called after then and catch. onSettled is not called with any params.

In this article, we will only implement .then() since the other two are pretty similar.

Okay, Let’s look at what we need to do

Promises have the following properties:

  • constructor((resolve, reject) => {}) — takes a handler function with the params resolve and reject .
  • .then(res => onFulfilled(res), err => onRejected(err)) — takes two params (onFulfilled, onRejected) which are functions Promise consumers calls to consume their promises after they are resolved or rejected
  • Promise’s states — “pending”, “fulfilled”, “rejected”
  • Promise’s value — the result or error called with functions onFulfilled and onRejected which are provided by the Promise’s consumer.

A basic version of Promise

When creating a Promise, the constructor accepts a handler function. The handler function executes and calls resolve when complete or reject if any error is encountered. Therefore, in our Promise class, we need the constructor to create the resolve and reject function which are passed to the handler.

Depending on the status of the Promise, we execute the callbacks. There are a few rules about the status.

  1. All Promises start with the “pending” status.
  2. Once a status has changed to “fulfilled” or “rejected”, it cannot be changed.

Here is our very first version of a Promise. If you see any obvious bugs, please bear with me. We will address them later.

This is a great start! But as you might have noticed, it has a pretty obvious bug. It does not support asynchronous execution (which is the main reason why people use Promises). If we change our test code like the following and use setTimeout to resolve the Promise, it will not work.

This is because when we call .then(), our Promise’s status is still pending. Neither onFulfilled nor onRejected was executed. We need to support asynchronous execution!

An improved version of Promise — async support

To support async, we need to store the onFulfilled and onRejected functions somewhere. Once the Promise’s status changes, we execute the functions immediately.

.then() can be called multiple times on the same Promise. Therefore, we will use two arrays ,onFulfilledCallbacks and onRejectedCallbacks , to store the functions and execute them once resolve or reject is called.

Here is our second version.

We are making great progress! I think if you are able to write this version in an interview, the interviewer should be satisfied. But we can wow them by implementing Promise chaining.

An even better Promise — chaining support

We know that we can chain Promises like so:

With the second version of our Promise, if we tried to run the above code, the code would print “resolved first one” and throw — “Uncaught TypeError: Cannot read property ‘then’ of undefined”. This is because our implementation of .then() does not return any value.

Let’s modify.then() to return a new Promise and resolve it with the return value of onFulfilled/onRejected.

Now if we test the Promise with the same code, we get both console.log outputs! This is great! But not quite there. What if the return value of onFulfilled/onRejected is a Promise? A real life use case would be: I fetched some data from a service, after I got a response, I need to use the data in the response to fetch additional data from another service.

Our current implementation will not work with the following code.

Because our Promise does not know how to handle the response of onFulfilled/onRejected if it is a Promise, we need to find a way to deal with it.

How to deal with it? — call .then()

Okay my head hurts…Hang in there! I promise this is the last bit.

Let’s straighten out the different Promises we have in play.

  • “first Promise” — the initial Promise, as in p1 in the above code.
  • “second Promise” — the Promise created and returned in.then()
  • “third Promise” — the Promise returned from onFulfilled/onRejected

Inside .then(), we created a new Promise (a.k.a. second Promise) to return to the caller. If the onFulfilled function returns a Promise (a.k.a third Promise), we will call .then(resolve, reject) on the third Promise to chain it. We pass in the second Promise’s resolve and reject functions so the second Promise can settle once the third Promise is settled.

In other words, the order of Promises settling is first -> third -> second.

Here is the updated .then().

That is it! Okay, let’s look at our code of final version! 😍

Final Version

Yay! We’re done!

Congratulations! You made it! I hope this has been helpful by us laying out the journey from a pretty barebones implementation to a pretty good final version. We talked about how to use a Promise, how to consume a Promise, and implemented async and chaining support.

Of course, there are so much more to Promise, such as Promise.all(), . Promise.race(), Promise.resolve(), Promise.reject(). I will find sometime to write another article about them in the future. UPDATE: here is the article — Implementing Promise.all, Promise.race, Promise.resolve and Promise.reject in Javascript.

This is my first time writing a Medium article. I’d love your feedback! Thank you for reading! ❤️

--

--