JS illustrated: Promises

John Kapantzakis
Oct 8 · 13 min read

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.

Callbacks

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 function passing a url path as first argument and a callback function as the second argument. The 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 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 ( ) 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

Promises

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.

and

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 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 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.

Getting control back

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.

Readable async code

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 API

The 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(reason)

creates an immediately rejected promise and it is a shorthand of the following code:

The next snippet shows that returns the same rejected promise with a traditionally constructed promise ( ) that gets immediately rejected with the same reason.

Promise.resolve(value)

creates an immediately resolved promise with the given value. It is a shorthand of the following code:

Comparing a promise constructed with the keyword and then, immediately resolved with value , to a promise constructed by with the same value, we see that both of them return identical results.

Thenables

According to Promises/A+ specification

thenable is an object or function that defines a method

Lets see a thenable in action in the following snippet. We declare the object that has a method which immediately calls the second function with the value as argument. As we can see, we can call the method of object passing two functions the second of which get called with the value as the first argument, just like a promise.

But what if we want to use the method as we do with promises?

Oops! En error indicating that the object does not have a method available occurs! That's normat because that's the case. We have declared a plain object with only one method, , 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 method, is a promise object.

But how can help with this situation?

can accept a thenable as its argument and then return a promise object. Lets treat our object as a promise object.

can be used as a tool of converting objects to promises.

Promise.all(iterable)

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, , and which they all get resolved after a specific amount of time. We intentionaly resolve before 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 , 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 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(iterable)

behaves like 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 gets resolved with an array of objects describing the status of the promises that were passed.

Promise.race(iterable)

waits for the first promise to be resolved or rejected and resolves, or rejects, respectively, the promise returned by with the value of that promise.

In the following example, promise resolved before got rejected.

If we change the delays, and set to be rejected at 100ms, before gets resolved, the final promise will be rejected with the respecive message, as shown in the following illustration.

Promise.prototype methods

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.

Promise.prototype.then()

We have already used many times in the previous examples. 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 call operates.

If the resolution handler of a call of a resolved promise is not a function, then no error is thrown, instead, the promise returned by carries the resolution value of the previous state.

In the following snippet, is resolved with value . Calling with no arguments will return a new promise with resolved state. Calling with an resolution handler and a valid rejection handler will do the same. Finally, calling 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 call of a rejected promise.

Lets see the following illustrations that present the flow of promises resolution or rejection using , assuming that is a resolved promise with value and 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 , the returned promise keeps the state ( ) 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 value.

Promise.prototype.catch()

We call when we want to handle rejected cases only. accepts a rejection handler as a parameter and returns another promise so it can be chained. It is the same as calling , providing an or resolution handler as the first parameter. Lets see the following snippet.

In the next illustration we can see the way that operates. Notice the second flow where we throw an error inside the resolution handler of the 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 block.

On the other hand, the last illustration shows the same case, with an additional at the end of the flow, that, actually, catches the error.

Promise.prototype.finally()

can be used when we do not care wheather the promise has been resolved or rejected, just if the promise has been settled. accepts a function as its first parameter and returns another promise.

The promise which is returned by the call is resolved with the resolution value of the initial promise.

Conclusion

Promises is a wide topic that cannot be fully covered by an article. I’ve tried to present some simple illustrations that will help the reader to get an idea of the way that promises work in Javascript.

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 😁


Originally published at https://dev.to on October 8, 2019.

Frontend Weekly

It's really hard to keep up with all the front-end development news out there. Let us help you. We hand-pick interesting articles related to front-end development. You can also subscribe to our weekly newsletter at http://frontendweekly.co

John Kapantzakis

Written by

Web developer that loves Javascript and tries to learn something new every day!

Frontend Weekly

It's really hard to keep up with all the front-end development news out there. Let us help you. We hand-pick interesting articles related to front-end development. You can also subscribe to our weekly newsletter at http://frontendweekly.co

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade