How to Create your own Promise
Ever wondered how a Promise works in JavaScript? Let’s implement one and see if we can understand it a little better.
What is a Promise?
A Promise is an object that can represent an asynchronous operation.
This object promises to return the result of this asynchronous operation, be it a success or a failure. If this operation never completes, it will wait till eternity or till it is garbage collected.
What we need is a medium to collect the result of this asynchronous operation from the Promise and that’s where then()
method steps into the limelight.
Thenables
A thenable is any object that has a then()
method implementation. Yeah, Promises are thenable.
From a Promise context, then()
method is used to add handlers which will eventually receive the result of the asynchronous operation that it represents.
Promise
.resolve( 'result' )
.then( successcallback, failurecallback );
The operation when completed leads to the promise getting settled with a resolved value
or a rejected error
.
If the operation completes successfully, the successcallback( value )
is triggered and the resolved value
is passed as the argument.
If the operation fails magnificently, the failurecallback( error )
is triggered and the rejected error
is passed as the argument.
A Promise thus can have only three states to represent the asynchronous operation, namely:
- pending: waiting for the asynchronous operation to complete.
- fulfilled: the operation is completed successfully.
- rejected: the operation has failed somehow.
When a Promise gets fulfilled or rejected, it becomes the final state of the Promise, thus making the Promise settled, happily ever after…
Once the promise is settled no further updates can be done to either the result or the state of the promise.
The Promise Constructor:
A Promise constructor creates the promise object for us:
new Promise( executor )
It expects an executor
function which ties the asynchronous activity to the Promise.
This executor
function is called by the Promise constructor with arguments resolve
and reject
which are methods provided by the Promise constructor to do the following:
resolve( data )
: set Promise state tofulfilled
and the result asdata
.reject( error )
: set Promise state torejected
and the result as anerror
.
Based on the asynchronous activity, the executor
function can then call either resolve
or reject
method to pass the result to the Promise.
A Promise once constructed has internal properties:
[[PromiseState]]
: current state of promise.[[PromiseResult]]
: holds the resolved value or the rejected reason.
Let’s look at an example:
Creating a PromiseCopy:
Let’s start with the constructor
. We receive the executor
and we need to pass resolve
and reject
methods which are utility functions.
resolve
and reject
are static methods in Promise which are used to create a new Promise as shown below:
When these methods are passed to the executor
function inside the Promise constructor, they behave differently. They are bind
to the Promise instance and returns undefined
.
With the above knowledge, let’s go ahead and create a PromiseCopy:
Promise.resolve():
Promise.resolve( value )
receives the success response which is set as the #result
of the Promise.
But if this value is a thenable object, Promise.resolve()
doesn’t set this value as the #result
.
if you pass a thenable object to
Promise.resolve()
, it will keep unwrapping the result by recursively callingPromise.resolve()
until it reaches the final value.
Once the Promise.resolve()
receives the final value, it updates the #result
of Promise and sets the state to fulfilled.
Once the Promise is fulfilled, we can dispatch the callbacks waiting eagerly for the #result
. Let’s write the resolve()
method keeping the above things in mind:
Promise.reject():
reject( reason )
method is pretty similar to the resolve()
method, except it doesn’t do any unwrapping of thenables. It goes straight ahead and sets the reason for the error in the #result
and set the #state
as rejected. Also, yeah dispatch the callbacks:
then() and Promise chaining:
A Promise then( successcallback, failurecallback )
is used to add callbacks. These callbacks are stored in the Promise instance and triggered once the Promise is settled.
Promise.then()
is also composable meaning that you can chain the then()
calls, and each then()
method receives the result of the previous asynchronous operation.
This is especially useful to avoid callback hell as you can chain dependent asynchronous tasks via Promise chaining.
then( successcallback, failurecallback )
always returns a new Promise which gets resolved by the return value of either thesuccesscallback
or thefailurecallback
depending on the#state
of the current Promise.
If the successcallback
or failurecallback
returns a Promise, the result is unwrapped by the resolve()
method of the new Promise before executing the next then()
method in the Promise chain.
Another important thing is if these callbacks are not passed, the current Promise #state
and #result
is forwarded to the new Promise.
Let’s look at the then()
implementation:
Please note above that an entry is pushed to #handlers
which will be triggered when the current Promise is settled.
Handlers
Handlers is an Array that contains callbacks attached using the then()
method. These handlers are kept in reference and triggered asynchronously once the Promise is settled.
Each handler contains methods:
success()
: executed when Promise is fulfilled.fail()
: executed when Promise is rejected.
We can create a #dispatchCallback()
method to check if the Promise is settled and eventually call the handlers.
Each handler should be triggered only once after the promise is settled and should be removed from reference afterwards.
As Promise handling is done in the micro-task queue, we can use the queueMicroTask
method to trigger the handlers asynchronously as a microtask.
We also will have a private property #queued
as a check to avoid duplicate triggers:
Log Promise Error
Promise if rejected logs a generic error if a failurecallback
is not provided to handle the error.
Although in promise chaining, a rejected Promise will be forwarded by the handler
till it reaches a Promise with no #handlers
. This essentially can be our check to trigger the error log as done above in #dispatchCallback()
.
catch():
We can attach failurecallback
using the catch()
method as well. However, we also need to forward the value to the next then()
method if the promise is fulfilled.
For this, we can reuse then()
method and also pass undefined
as successcallback
. Fairly simple right :
finally():
finally( finallycallback )
method gets implemented regardless of whether the Promise gets fulfilled or rejected. But it’s not as simple as just passing the finallycallback
to then( finallycallback, finallycallback )
.
The peculiar thing about finally()
is that the current Promise's #result
is not passed to the finallycallback
. This #state
and #result
of the current Promise is passed on to the new Promise returned by finally()
.
If the finallycallback
is asynchronous, meaning if it returns a Promise or a thenable, the next callback in the Promise chain will only be triggered after this operation is complete.
In a way, the finallycallback
is just some do it anyway intermediate task sitting in between the Promise chaining which has to be successful.
Because if there’s an error in finallycallback
, the new Promise returned by finally()
is rejected with this error.
Let's take a look at the implementation:
Conclusion:
Understanding how a Promise works can be hugely beneficial while implementing asynchronous tasks in JavaScript. I hope this article helps you create your own Promise and then wait for it asynchronously.
You can also check out the below link to play with PromiseCopy
: