This is the second JS illustrated article I've wrote. The first one was about the event loop
ES6 (ECMAScript 2015) has introduced a new feature called Promise. There are numerous excelent articles and books that explain the way that Promises work. In this article, we are going to try to provide a simple and understandable description of how Promises work, without digging into much detail.
Before we start explaining what a promise is and how it works, we need to take a look at the reason of its existence, in order to understand it correctly. In other words, we have to identify the problem that this new feature is trying to solve.
Promises are inextricably linked to asynchrony. Before Promises, developers were able to write asynchronous code using callbacks. A callback is a function that is provided as parameter to another function, in order to be called, at some point in the future, by the latter function.
Lets take a look at the following code
We are calling
ajaxCall function passing a url path as first argument and a callback function as the second argument. The
ajaxCall function is supposed to execute a request to the provided url and call the callback function when the response is ready. In the meanwhile, the program continues its execution (the
ajaxCall does not block the execution). That's an asynchronous piece of code.
This works great! But there are some problems that might arise, like the following (Kyle Simpson, 2015, You don't know JS: Async & Performance, 42):
- The callback function never gets called
- The callback function gets called too early
- The callback function gets called too late
- The callback function gets called more than once
These problems might be more difficult to be solved if the calling function (
ajaxCall) is an external tool that we are not able to fix or even debug.
It seems that a serious problem with callbacks is that they give the control of our program execution to the calling function, a state known as inversion of control (IoC).
The following illustration shows the program flow of a callback based asynchronous task. We assume that we call a third party async function passing a callback as one of its parameters. The red areas indicate that we do not have the control of our program flow in these areas. We do not have access to the third party utility, so the right part of the illustration is red. The red part in the left side of the illustration indicates that we do not have the control of our program until the third party utility calls the callback function we provided.
But wait, there's something else, except from the IoC issue, that makes difficult to write asynchronous code with callbacks. It is known as the callback hell and describes the state of multiple nested callbacks, as shown in the following snippet.
As we can see, multiple nested callbacks makes our code unreadable and difficult to debug.
So, in order to recap, the main problems that arise from the use of callbacks are:
- Losing the control of our program execution (Inversion of Control)
- Unreadable code, especially when using multiple nested callbacks
Now lets see what Promises are and how they can help us to overcome the problems of callbacks.
According to MDN
The Promise object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value.
A Promise is a proxy for a value not necessarily known when the promise is created
What's new here is that asynchronous methods can be called and return something immediately, in contrast to callbacks where you had to pass a callback function and hope that the async function will call it some time in the future.
But what's that it gets returned?
It is a promise that some time in the future you will get an actual value.
For now, you can continue your execution using this promise as a placeholder of the future value.
Lets take a look at the constructor
We create a Promise with the
new Promise() statement, passing a function, called the executor. The executor gets called immediately at the time we create the promise, passing two functions as the first two arguments, the resolve and the reject functions respectively. The executor usually starts the asynchronous operation (the
setTimeout() function in our example).
The resolve function is called when the asynchronous task has been successfully completed its work. We then say that the promise has been resolved. Optionally yet very often, we provide the result of the asynchronous task to the resolve function as the first argument.
In the same way, in case where the asynchronous task has failed to execute its assigned task, the reject function gets called passing the error message as the first argument and now we say that the promise has been rejected.
The next illustration presents the way that promises work. We see that, even if we use a third party utility, we still have the control of our program flow because we, immediately, get back a promise, a placeolder that we can use in place of the actual future value.
According to Promises/A+ specification
A promise must be in one of three states: pending, fulfilled, or rejected
When a promise is in pending state, it can either transition to the fullfilled (resolved) or the rejected state.
What's very important here is that, if a promise gets one of the fulfiled or rejected state, it cannot change its state and value. This is called immutable identity and protects us from unwanted changes in the state that would lead to undescoverable bugs in our code.
As we saw earlier, when we use callbacks we rely on another piece of code, often writen by a third party, in order to trigger our callback function and continue the execution of the program.
With promises we do not rely on anyone in order to continue our program execution. We have a promise in our hands that we will get an actual value at some point in the future. For now, we can use this promise as a placeholder of our actual value and continue our program execution just as we would do in synchronous programming.
Promises make our code more readable compared to callbacks (remember the callback hell?). Check out the following snippet:
We can chain multiple promises in a sequential manner and make our code look like synchronous code, avoiding nesting multiple callbacks one inside another.
Promise object exposes a set of static methods that can be called in order to execute specific tasks. We are going to briefly present each on of them with some simple illustrations whenever possible.
Promise.reject() creates an immediately rejected promise and it is a shorthand of the following code:
The next snippet shows that
Promise.reject() returns the same rejected promise with a traditionally constructed promise (
new Promise()) that gets immediately rejected with the same reason.
Promise.resolve() creates an immediately resolved promise with the given value. It is a shorthand of the following code:
Comparing a promise constructed with the
new keyword and then, immediately resolved with value
1, to a promise constructed by
Promise.resolve() with the same value, we see that both of them return identical results.
According to Promises/A+ specification
thenable is an object or function that defines a
Lets see a thenable in action in the following snippet. We declare the
thenable object that has a
then method which immediately calls the second function with the
"Rejected" value as argument. As we can see, we can call the
then method of
thenable object passing two functions the second of which get called with the
"Rejected" value as the first argument, just like a promise.
But what if we want to use the
catch method as we do with promises?
Oops! En error indicating that the
thenable object does not have a
catch method available occurs! That's normat because that's the case. We have declared a plain object with only one method,
then, that happens to conform, in some degree, to the promises api behaviour.
In any case, it doesn't mean that an object which exposes a
thenmethod, is a promise object.
But how can
Promise.resolve() help with this situation?
Promise.resolve() can accept a thenable as its argument and then return a promise object. Lets treat our
thenable object as a promise object.
Promise.resolve() can be used as a tool of converting objects to promises.
Promise.all() waits for all promises in the provided iterable to be resolved and, then, returns an array of the values from the resolved promises in the order they were specified in the iterable.
In the following example, we declare 3 promises,
p3 which they all get resolved after a specific amount of time. We intentionaly resolve
p1 to demonstrate that the order of the resolved values that get returned, is the order that the promises were declared in the array passed to
Promise.all(), and not the order that these promises were resolved.
In the upcoming illustrations, the green circles indicate that the specific promise has been resolved and the red circles, that the specific promise has been rejected.
But what happens if one or more promises get rejected? The promise returned by
Promise.all() gets rejected with the value of the first promise that got rejected among the promises contained in the iterable.
Even if more than one promises get rejected, the final result is a rejected promise with the value of the first promise which was rejected, and not an array of rejection messages.
Promise.allSettled() behaves like
Promise.all() in the sence that it waits far all promises to be fullfiled. The difference is in the outcome.
As you can see in the above snippet, the promise returned by the
Promise.allSettled() gets resolved with an array of objects describing the status of the promises that were passed.
Promise.race() waits for the first promise to be resolved or rejected and resolves, or rejects, respectively, the promise returned by
Promise.race() with the value of that promise.
In the following example,
p2 promise resolved before
p1 got rejected.
If we change the delays, and set
p1 to be rejected at 100ms, before
p2 gets resolved, the final promise will be rejected with the respecive message, as shown in the following illustration.
We are now going to take a look at some methods exposed by the promise's prototype object. We have already mentioned some of them previously, and now, we are going to take a look at each one of them in more detail.
We have already used
then() many times in the previous examples.
then() is used to handle the settled state of promises. It accepts a resolution handler function as its first parameter and a rejection handler function as its second parameter, and returns a promise.
The next two illustrations present the way that a
then() call operates.
If the resolution handler of a
then() call of a resolved promise is not a function, then no error is thrown, instead, the promise returned by
then() carries the resolution value of the previous state.
In the following snippet,
p1 is resolved with value
then() with no arguments will return a new promise with
p1 resolved state. Calling
then() with an
undefined resolution handler and a valid rejection handler will do the same. Finally, calling
then() with a valid resolution handler will return the promise's value.
The same will happen in case that we pass an invalid rejection handler to a
then() call of a rejected promise.
Lets see the following illustrations that present the flow of promises resolution or rejection using
then(), assuming that
p1 is a resolved promise with value
p2 is a rejected promise with reason
We see that if we don't pass any arguments or if we pass non-function objects as parametes to
then(), the returned promise keeps the state (
resolved / rejected) and the value of the initial state without throwing any error.
But what happens if we pass a function that does not return anything? The following illustration shows that in such case, the returned promise gets resolved or rejected with the
catch() when we want to handle rejected cases only.
catch() accepts a rejection handler as a parameter and returns another promise so it can be chained. It is the same as calling
then(), providing an
null resolution handler as the first parameter. Lets see the following snippet.
In the next illustration we can see the way that
catch() operates. Notice the second flow where we throw an error inside the resolution handler of the
then() function and it never gets caught. That happens because this is an asynchronous operation and this error wouldn't have been caught even if we had executed this flow inside a
On the other hand, the last illustration shows the same case, with an additional
catch() at the end of the flow, that, actually, catches the error.
finally() can be used when we do not care wheather the promise has been resolved or rejected, just if the promise has been settled.
finally() accepts a function as its first parameter and returns another promise.
The promise which is returned by the
finally() call is resolved with the resolution value of the initial promise.
If you find any errors or ommisions, please do not hestitate to mention them! I've put a lot of effort to write this article and I've learned many things about promises. I hope you liked it 😁
- MDN: Promise
- Kyle Simpson, 2015, You don't know JS: Async & Performance, 29-119