Visualization of Promises and Async / Await



Good day, friends!

I present to you the translation of the article "JavaScript Visualized: Promises & Async / Await" by Lydia Hallie.

Have you come across JavaScript code that ... does not work as expected? When functions are performed in an arbitrary, unpredictable order, or are performed with a delay. One of the main tasks of promises is to streamline the execution of functions.

My insatiable curiosity and sleepless nights paid off in full - thanks to them I created several animations. It's time to talk about promises: how they work, why they should be used, and how it's done.

Introduction


When writing JS code, we often have to deal with tasks that depend on other tasks. Suppose we want to get an image, compress it, apply a filter to it and save it.

First of all, we need to get an image. To do this, use the function getImage. After loading the image, we pass it to the function resizeImage. After compressing the image, we apply a filter to it using the function applyFilter. After compression and applying the filter, we save the image and inform the user about the success.

As a result, we obtain the following:



Hmm ... Have you noticed anything? Despite the fact that everything works, it does not look the best way. We get many nested callback functions that depend on previous callbacks. This is called callback hell and it makes it very difficult to read and maintain code.

Fortunately, today we have promises.



Promise Syntax


Promises were introduced in ES6. In many manuals you can read the following:

A promise (promise) is a value that is fulfilled or rejected in the future.

Yeah ... So-so explanation. At one time, it made me consider promises to be something strange, vague, some kind of magic. What are they really?

We can create a promise with a constructor Promisethat takes a callback function as an argument. Cool, let's try:



Wait, what's coming back here?

PromiseIs an object that contains status ( [[PromiseStatus]]) and value ( [[PromiseValue]]). In the above example, the value [[PromiseStatus]]is pending, and the value of the promise is undefined.

Do not worry, you do not have to interact with this object, you can not even access the [[PromiseStatus]]and properties [[PromiseValue]]. However, these properties are very important when working with promises.

PromiseStatus or the status of a promise can take one of three values:

  • fulfilled: resolved (). ,
  • rejected: rejected (). -
  • pending: , pending (, )

Sounds great, but when does a promise get the indicated statuses? And why does status matter?

In the above example, we pass a Promisesimple callback function to the constructor () => {}. This function actually takes two arguments. The value of the first argument, usually called resolveor res, is the method invoked when the promise is executed. The value of the second argument, usually called rejector rej, is the method called when the promise is rejected when something went wrong.



Let's see what is output to the console when calling the methods resolveand reject:



Cool! Now we know how to get rid of status pendingand meaning undefined. The status of the promise when calling the method resolveis fulfilled, when reject-rejected.

[[PromiseValue]]or the value of the promise is the value that we pass to the methods resolveor rejectas an argument.

Fun fact: Jake Archibald after reading this article pointed to a bug in Chrome, which instead fulfilledreturned resolved.



Ok, now we know how to work with the object Promise. But what is it used for?

In the introduction, I gave an example in which we get an image, compress it, apply a filter to it and save it. Then it all ended with callback hell.

Fortunately, promises help to cope with this. We rewrite the code so that each function returns a promise.

If the image has loaded, we perform a promise. Otherwise, if an error occurs, reject the promise:



Let's see what happens when this code is run in the terminal:



Cool! Promis returns with parsed (“parsed”) data, as we expected.

But ... what's next? We are not interested in the subject of promis, we are interested in its data. There are 3 built-in methods for obtaining a Promise value:

  • .then(): called after performing a promise
  • .catch(): called after the rejection of the promise
  • .finally(): always called, both after execution and after rejection of a promise



The method .thentakes the value passed to the method resolve:



The method .catchtakes the value passed to the method reject:



Finally, we got the desired value. We can do anything with this value.

When we are confident in the fulfillment or rejection of a promise, you can write Promise.resolveeither Promise.rejectwith the appropriate value.



This is the syntax that will be used in the following examples.



The result .thenis the value of the promise (i.e., this method also returns the promise). This means that we can use as much .thenas needed: the result of the previous .thenis passed as an argument to the next .then.



In getImagewe can use several .thento transfer the processed image to the next function.



This syntax looks much better than the ladder of nested callback functions.



Microtasks and (macro) tasks


Well, now we know how to create promises and how to extract values ​​from them. Add some code to our script and run it again:



First, it is displayed in the console Start!. This is normal, since we have the first line of code console.log('Start!'). The second value displayed on the console is End!, not the value of the completed promise. The value of the promise is displayed last. Why did it happen?

Here we see the power of promises. Although JS is single-threaded, we can make the code asynchronous with Promise.

Where else could we observe asynchronous behavior? Some methods built into the browser, such as setTimeout, can simulate asynchrony.

Right In the event loop (Event Loop), there are two types of queues: the queue of (macro) tasks or simply tasks ((macro) task queue, task queue) and the queue of microtasks or just microtasks (microtask queue, microtasks).

What applies to each of them? In short, then:

  • Macro Tasks: setTimeout, setInterval, setImmediate
  • Microtasks: process.nextTick, Promise callback, queueMicrotask

We see Promisein the list of microtasks. When the Promisemethod is executed and called then(), catch()or finally(), the callback function with the method is added to the microtask queue. This means that the callback with the method is not executed immediately, which makes JS code asynchronous.

When is the method then(), catch()or finally()is it executed? Tasks in the event loop have the following priority:

  1. First, the functions in the call stack are executed. The values ​​returned by these functions are removed from the stack.
  2. After the stack is freed up, microtasks are placed into it one after another (microtasks can return other microtasks, creating an endless cycle of microtasks).
  3. After the stack and the microtask queue are released, the event loop checks for macros. Macro tasks are pushed onto the stack, executed, and deleted.



Consider an example:

  • Task1: A function that is added to the stack immediately, for example, by a call in code.
  • Task2, Task3, Task4: Mikrozadachi example thenProMIS or task added via queueMicrotask.
  • Task5, Task6: macro tasks, for example, setTimeoutorsetImmediate



First Task1returns a value and is removed from the stack. Then the engine checks for microtasks in the corresponding queue. After adding and subsequently removing microtasks from the stack, the engine checks for macro tasks, which are also added to the stack and removed from it after returning the values.

Enough words. Let's write the code.



In this code, we have a macro setTimeouttask and a micro task .then. Run the code and see what is displayed in the console.

Note: in the above example, I use methods such as console.log, setTimeoutand Promise.resolve. All of these methods are internal, so they do not appear in the stack trace - do not be surprised when you do not find them in the browser troubleshooting tools.

In the first line we haveconsole.log. It is added to the stack and displayed in the console Start!. After that, this method is removed from the stack and the engine continues to parse the code.



The engine reaches setTimeout, which is added to the stack. This method is a built-in browser method: its callback function ( () => console.log('In timeout')) is added to the Web API and is there before the timer fires. Despite the fact that the timer counter is 0, the callback is still placed first in the WebAPI and then in the macro task queue: setTimeout- this is a macro task.



Next, the engine reaches the method Promise.resolve(). This method is added to the stack, and then executed with a value Promise. His callback thenis placed in the microtask queue.



Finally, the engine reaches the second method console.log(). It is immediately pushed onto the stack, it is output to the consoleEnd!, the method is removed from the stack, and the engine continues.



The engine “sees” that the stack is empty. Checking the task queue. It is located there then. It is pushed onto the stack, the value of the promise is displayed on the console: in this case, a string Promise!.



The engine sees that the stack is empty. He "looks" in the queue of microtasks. She is empty too.

It's time to check the macro task queue: it's there setTimeout. It is pushed onto the stack and returns a method console.log(). A string is output to the console 'In timeout!'. setTimeoutis removed from the stack.



Done. Now everything fell into place, right?



Async / await


ES7 introduced a new way to work with asynchronous code in JS. Using the keywords asyncand awaitwe can create an asynchronous function that implicitly returns a promise. But ... how do we do this?

Earlier, we discussed how to explicitly create an object Promise: using new Promise(() => {}), Promise.resolveor Promise.reject.

Instead, we can create an asynchronous function that implicitly returns the specified object. This means that we no longer need to manually create Promise.



The fact that an asynchronous function implicitly returns a promise is, of course, great, but the power of this function is fully manifested when using a keyword await.awaitmakes the asynchronous function wait for the promise (its value) to complete. To get the value of the performed promise, we must assign the variable the expected (awaited) value of the promise.

It turns out that we can delay the execution of an asynchronous function? Great, but ... what does that mean?

Let's see what happens when you run the following code:







At first the engine sees console.log. This method is pushed onto the stack and displayed on the console Before function!.



Then the asynchronous function is called myFunc(), its code is executed. In the first line of this code, we call the second console.logwith a line 'In function!'. This method is added to the stack, its value is displayed on the console, and it is removed from the stack.



The function code is executed next. On the second line we have a keyword await.

The first thing that happens here is the execution of the expected value: in this case, the function one. It is pushed onto the stack and returns a promise. After the promise has been completed, and the function onereturned the value, the engine sees await.

After that, the execution of the asynchronous function is delayed. The execution of the function body is suspended, the remaining code is executed as a microtask.



After the execution of the asynchronous function has been delayed, the engine returns to the execution of the code in a global context.



After all the code has been executed in a global context, the event loop checks for microtasks and detects myFunc(). myFunc()pushed onto the stack and executed.

The variable resgets the value of the executed promise returned by the function one. We call console.logwith the value of the variable res: a string One!in this case. One!It is displayed in the console.

Done. Notice the difference between the asynchronous function and the thenpromis method ? Keywordawaitpostpones the execution of an asynchronous function. If we used then, then the Promise body would continue to run.



It turned out pretty verbose. Do not worry if you feel insecure when working with promises. It takes some time to get used to them. This is common to all techniques for working with asynchronous code in JS.

Also see "Visualization of the work of service workers . "

Thank you for your time. I hope it was well spent.

All Articles