First, well give a brief introduction / refresher to what Promises are and how to use them, then move onto an analysis of how to create one from scratch, notes on implementation with code examples, and finally the full implementation with some test cases.
Before proceeding to how to create a Promise from scratch, let's briefly review what a Promise is and how it works. A Promise is an object which handles an asynchronous task, provided by the consumer, and allows consumers to respond to the completion or failure of that task. In a more technical sense, it is a proxy for a value which might not yet be known when the promise is created. Promises will always be in a state of
rejected. When the
rejected state is reached, the consumer can use the emitted value directly or use it as a part of a chain involving multiple promises.
The code below shows one common way of chaining promises:
The code below shows another way of chaining promises in a more implicit manner:
- Implement the constructor so the injected constructor callback invokes resolve and reject.
- Allow any number of
.catchcallbacks to be added.
- Return a new promise when
- Implement a
.finallymethod which will be invoked regardless of the final state of the Promise.
- Ensure the Promise correctly invokes resolve & reject + all callbacks registered.
All of the points above refer to handling callbacks in different ways. We will need to be careful in how we accept, store, and later invoke callbacks we receive at each point. In our implementation here, we will invoke the constructor function immediately, but store all other callbacks to be invoked at a later point.
In our implementation here, we will initialize empty arrays for callbacks of three types:
- Resolve: callbacks registerd with
- Reject: callbacks registered with
- Finally: callbacks registered with
When each function, such as
.then, is invoked, we will:
- Return a new custom Promise object with the logic below wrapped in:
- Push a new wrapper-callback in one of our callback arrays. The wrapper-callback will:
- Run the original callback provided by the consumer and resolve / reject with that value.
- Push another wrapper-callback in the other callback array to handle the other scenario.
- For example, on
.catch, pass a callback for reject and resolve so the value is piped through either way.
- For example, on
The code below represents a partial implementation of the custom Promise object. It creates the empty callback arrays, then on each
.then type method, follows the logic above. With this logic, our Promise object will handle the callbacks and chain Promises appropriately. Notice that then
.catch have slightly different logic when handling the callback provided by the user, but the overall logic is the same.
Invoking the Constructor Callback
Unlike the other callbacks, the constructor callback must be invoked immediately (synchronously). In the example below, we can see that the console logging provided in the constructor callback logs to the console before the statement immediately following it:
Therefore, in our custom Promise object, we will need to invoke the callback, which we are calling the
initializer, immediately. In order to do so, we will need to have a callback for
reject ready to pass into that initializer function. We will define methods
_reject on the class itself, then wrap them in callbacks which will be passed into the initializer function. Those two class methods will invoke all relevant pending callbacks:
resolve callbacks upon resolve and
reject callbacks upon reject. In any case, all
finally callbacks will be invoked.
Putting the Pieces Together
When the two parts, constructor invokation and
.then callback handling, are put together, the main parts of the Promise implementation will be complete. In the code here, promise.mjs contains all of the code discussed above, plus a few other things not explicitly mentioned, and tests.mjs has a few different test cases to ensure the core functionality is working properly. There are certain thing snot included, suc has
Promise.race, but for the purposes of this example, our implementation covers the main points.
Note: if you look through the test cases, you may observe that we are using
async / await with our own custom Promise class. Even though we are not using the built-in Promise, we can still take advantage of async and await because our Promise is still a thenable: something with a
.then method. All Promises are thenables, but not all thenables are Promises.
More on Promises
A more complete specification of the internal workings of Promises, along with browser compatability, can be found on Mozilla's Developer Site. Note that all modern major browsers do support Promises, but there may be slight variations in coverage for certain Promise methods.
Full Code Implementation
Below, we'll showcase the full code contents which brings everything together: