Deep deep deep into Promises
You probably know how to use Promises, but do you know how they actually work?
To have everybody on the same page, let’s start from the basics. If you know what a promise is and how to use it, you can skip this part and jump right to the place where “the magic begins”.
What is a Promise?
When someone asks you for some data that you do not have right now, you can promise to send it later or send an error if something goes wrong.
So a Promise is an object representing the eventual completion or failure of an asynchronous operation.
How to create a Promise?
Use Promise constructor.
The function passed to the constructor is called the “executor” function. It contains the code which should eventually produce the result. The executor runs automatically when ‘new Promise’ is created.
The ‘promise’ object created by the Promise constructor has the following properties:
- state — initially “pending”, then changes to either “fulfilled” when resolve is called or “rejected” when reject is called.
- result — initially undefined, then changes to value when resolve(value) is called or error when reject(error) is called.
How to get actual data from ‘promise’?
You can subscribe to data from a promise using ‘then’ consuming function.
The first argument of .then is a function that runs when the promise is resolved and receives the result.
The second argument of .then is a function that runs when the promise is rejected and receives the error.
The then method returns a promise, that’s why promises can be chained:
Promise static methods
Promise object has the following static methods:
- Promise.all(iterable) — wait for all promises to be resolved, or for any to be rejected.
- Promise.allSettled(iterable) — wait until all promises have settled (each may resolve or reject).
- Promise.any(iterable) — takes an iterable of Promise objects and, as soon as one of the promises in the iterable fulfills, returns a single promise that resolves with the value from that promise.
- Promise.race() — wait until any of the promises is fulfilled or rejected.
- Promise.reject(reason) — returns a new Promise object that is rejected with the given reason.
- Promise.resolve() — returns a Promise object that is resolved with a given value. If the value is a promise or “thenable” object, that promise is returned; otherwise, the returned promise will be fulfilled with the value.
Let’s look deeper at Promise.resolve()
According to the definition, the below statements are true:
Promise.resolve(5) -> returns promise<fulfilled>(5)Promise.resolve(Promise.resolve(5)) -> returns promise<fulfilled>(5)
This is where the magic begins
We are going to look deeper into how and when the promise’s state changes from ‘pending’ to ‘fulfilled’.
Let’s create a simple promise object that resolves a plain value immediately:
Which promise state do you expect to see?
Right, it is a fulfilled promise with value 5.
Now, keep in mind the Promise.resolve() usage example “Promise.resolve(Promise.resolve(5)) -> returns promise<fulfilled>(5)” and consider the following piece of code:
You expect to see a fulfilled promise again as for Promise.resolve(), right?
We will disappoint you. The resulting promise will have a ‘pending’ state.
Now consider you output the same promise from the above example, but with a delay:
Bingo! Promise now is in a fulfilled state.
Let’s summarise the results of our experiments:
- Promise.resolve returns a fulfilled promise for plain values.
- Promise.resolve returns a fulfilled promise when the value passed is a fulfilled promise.
- Resolve function passed to the executor returns a fulfilled promise for plain values.
- Resolve function passed to the executor returns a pending promise when the value passed is a fulfilled promise.
- The delayed result of the resolve function passed to the executor is a fulfilled promise when the value passed is a fulfilled promise.
What conclusions can we draw from this?
Obviously Promise.resolve() and resolve() work differently.
- Promise.resolve and resolve function work the same way for plain values. Both returns fulfilled promises.
- Promise.resolve and resolve functions work differently when a promise is passed as an argument. Promise.resolve returns the same promise passed as an argument when resolve works in some strange way.
- resolve returns pending promise for any promised values regardless of their state. And may change the returned promise’s state in some async way.
Does it mean that the resolve function works asynchronously when a promise is passed as an argument? To understand this let’s take a look at a part of Promises that definitely works asynchronously — consuming functions then, catch and finally.
You probably heard about the microtasks queue used by Promises consuming functions. If not, let’s do a quick reminder.
When the resolve function takes a plain value as an argument, it fulfills immediately. But if the argument is a promise (no matter what the state of the promise is), it subscribes to it and this consumer function is put into the microtask queue — until the initial promise is ready and the engine is free to execute the consumer. That’s why you won’t get a fulfilled promise right away, but get it inside the setTimeout.
This behavior is described in the specification:
When a promise resolve function is called with resolution argument, the following steps are taken:
The behavior of the promise differs based on the argument passed.
Point 8 says if the resolution is not an object (plain value) then FullfillPromise. Point 12 says if the resolution is not a then callable object, return FullfillPromise.
A promise is a then callable object, so we skip points 8 and 12, and go ahead to points 13-16 which basically say to subscribe and follow the promise passed.
In this article, we tried to reveal the nuances of using Promises and show clearly what pitfalls you may have.
Intspirit‘s team wishes you to make high-quality code that will be efficient and delight your customers.