This article was created by our dev Bartosz. It is also the next part of the bigger series, all the previous parts you can find here :).
A lot of water in the river has passed since my last posts about Javascript. It was not easy to find time for the next part. But ... I succeeded and would like to share something that has been bothering me for quite some time.
If you read my posts, you might remember that I mentioned that javascript works synchronously and is a single-threaded language. And what about this asynchronicity? What about AJAX requests? What about fetching data from external APIs asynchronously? I decided to go over all of this and break it down to the smallest pieces to present it in a most approachable way. In a way that I would love to learn from. Let's get started.
Single-threaded Synchronous
I know that I have already written about it but I would like everything to be at hand. I hope that after reading what I want to tell you, you will not feel the need to look elsewhere to find out about the basics of the synchronous and asynchronous facets of your Javascript.
So what about this single-threadedness and the synchronicity of Javascript? It all boils down to two very essential functionalities of the language. Namely, no matter how many lines our code has, everything we wrote will be done one line after another and one command at a time. Javascript has one thread in which the commands written by us are executed and the next one will be performed only when the previous one is completed. The end. Hey, but the asynchroninicity... - you might ask. I know, I know, but please wait a bit. Now it's time for my favorite console.logs.
Even in this case, when we do not perform complicated tasks, the command console.log('World') will be executed only when the first one - console.log ('Hello') is completed. One thread, synchronous. However, my most beloved example is below, which although when looking at it, it seems that it should return the true, it does not.
This is because the JavaScript engine does not look at this expression (3 > 2 > 1) as a whole. Of course, in the first step, it estimates 3 > 2 which returns true thanks to basic math. This true is parsed to the number of 1 because we do not know if a true is greater than the number of 1. In the end, the engine looks at the 1 > 1 which is false therefore result of the whole expression is false.
So once again in a nutshell - one thread, synchronous.
After this thrilling introduction, which was really just a reminder, we are moving to ... another repetition of what I have already written in previous posts (Execution Context: Execution Phase, Hoisting, Execution Context: Creation Phase, Invocation, Execution Stack). Execution Context and Execution Stack (also known as Call Stack). The first one appears every time we invoke the function and has its information about for example variables. The latter is simply the stack on which the Execution Contexts of the called functions are pushed. In this case, however, no words depict it as well as a few lines of code.
Why so many console logs? Well, I think that nothing will present it better than just logging what happens at the moment. When we run the script, all variables and function statements will be stored in memory (hoisting ladies and gentlemen), and then the code will start to execute (Execution Phase). I will use chrome dev tools and put in a few breakpoints, thanks to which we will be able to pause the execution of the script at any time.
As you can see, I set them at the beginning and end of each function and the logs will inform us when the function code began to execute and when it ends.
As the code is executed from top to bottom, nothing will happen until the line 23 ... and then boom, the first console.log appears.
Breakpoint set at the beginning of the function one (attention! Breakpoints pause BEFORE the marked line, in this case BEFORE console.log!) informed us that console.log('Im about to invoke one function!') and one(myNumber) was executed. On the other hand, console.log('I just finished everything!') did not show even though it is a line below. Why? Why does it look like if we were at the 4th line? Because the Execution Context of invoked function was pushed on the stack and everything after that does not matter to the JavaScript engine at the moment.
Here we have a blue arrow that shows us in which Execution Context currently executing code is. Ok, now let's get to the next breakpoint. Will it be in line 7?
Well, it turns out that we are already in the function two and nothing that was after the invocation of this function has been called. So…
... must wait. On the other hand, the Execution Context of function two lands on the stack.
In it, the function three is called and everything works the same as in the first case. The last console.log has to wait because we have arrived in the Execution Context of function three.
The matter here is simple. We do not invoke anything (in function three), so the whole thing is over now. In the console we have:
Ok, what about the rest of the code? Do we forget about it? Of course not. Since we will not create a new Execution Context at this stage, when everything is done in it, it will automatically be poped from our stack and...
... we will come back to …
So we are back in the Execution Context of function two, and it turns out that there is still something to do. The closing console.log is printed and as above, we pop the Execution Context from the stack. The last one remains.
Here, everything that's left is getting done.
And since everything has been done, the stack is empty! Phew, lots of pictures behind us so maybe now is the time for something that is not a repeat?!
I mentioned above that I wanted everything to be in one post but there is one more reason why I decided to do this "small" reminder. Now imagine that when visiting various websites, the example discussed above is the only way the websites operate. Something must be done for the next thing to be started. You probably agree that it would be very burdensome from the user's perspective. A good example is something like that.
The function waitFiveSeconds does what it's called - it waits five seconds. Because JavaScript is single-threaded and synchronous, it does not matter how many time we would click the mouse in the first 5 seconds. In the end, the result would be:
Believe me, I tried very hard. As I wrote above - it would be very tedious. However, fortunately, JavaScript activity in the browser is more complex and what happens in the main and only JavaScript thread is not the only thing that actually takes place in the browser. It would be too boring right?
However, what did the above example show us? Mainly that blocking what is supposed to happen in the main JavaScript thread is very dangerous. If something that takes some time to execute would land on the stack, it could easily break the user's experience of visiting our website. Since we already know that it should not be done, what can we do instead? The answer is simple - Asynchronous Callbacks.
In the above example, we have two functions. One is invoked on click (onClick) and calls setTimeout(). setTimeout in our case accepts two parameters. The first is the function (also called a callback function) that we want to invoke. The second tells how long will it take to invoke the passed callback. This time, clicking on the browser window will result in this:
In the above example, we have two functions. One is invoked on click (onClick), which implies setTimeout(). setTimeout in our case accepts two parameters. The first is the function (also called a callback function) that we want to invoke. The second tells how long will it take to invoke the passed callback. This time, clicking on the browser window will get something like that:
As I mentioned above, many things can happen in the browser, some of them being the event loop, the task queue, and web APIs. The latter, eg ajax requests, setTimeout or DOM (document itself) communicate with the stack and task queue. Let's take the last piece of code as an example. Some might have been surprised that the code seemed to go away without waiting for the waitFiveSeconds function. This is because setTimeout uses the timer API. The whole action is put away from the stack for the time equivalent of the number of ms we gave as the second parameter. Besides, setTimeout callbacks are asynchronous. Of course, not all callbacks in JavaScript act like that. Even more so, most of them are synchronous. For example callbacks that we pass to array methods like map() or filter(). However, some behave asynchronously. The simplest and most often used in examples is setTimeout. It lets us simulate fetching data from the server.
As the function waitFiveSeconds was temporarily released from the stack, "I was clicked!" appeared in the console. After 5s, the function waitFiveSeconds will be pushed from the web APIs to the task queue
Task queue is nothing more than a queue on which tasks are pushed. Nothing prevents you from queuing up more than one task. Of course, we do not want the asynchronous functions to be forgotten, so we have to somehow redirect them back to the stack. Fortunately, we do not have to solve it personally - if our stack is empty (meaning nothing is to be done, no execution context has been created) and our task queue is not empty, the first thing is pushed out of the task queue. As the function waitFiveSeconds was temporarily released from the stack, "I was clicked!" appeared in the console. After 5s, the function waitFiveSeconds will be pushed from the web APIs to the task queue.
The most common example of using setTimeout is when we set the second parameter to 0. After a slight change in the last code, what do you think will happen?
Precisely, the result is very similar to the previous one.
This is because function waitZeroSeconds has been postponed and will only be executed when our stack becomes empty. Considering that, after a click, our stack cannot be empty because the Execution Context of the function onClick lands on top of it. Only after everything that has been initialized in it is popped off the stack (in our case - console.log ('I was clicked')), the function waitZeroSeconds will be performed.
Since we already know about things like JavaScript asynchronicity in the browser and how synchronous JavaScript deal with this, let's look at an example in which we have two simple queries to an external API, and two console.logs outside them.
Fetch is a modern replacement for the XMLHttpRequest and takes place asynchronously. It is used to send requests to API to retrieve data.
The matter is simple. At the beginning in the console we get:
Since we know that fetch works asynchronously, we'll get a console.logs after the stack is cleared. The question is, however, in what order? (hint: check the number that indicates the amount of data fetched).
So?
Everything nice and easy, right? Almost. In the example above, a thought arises - what if we would like to get the data first and use it in the second query? Let's take a look at another case.
Note that we do not know the id of the post and send the request to endpoint ... / posts / 1 to actually get it. Then we want to use the saved id and retrieve all comments belonging to this id.
Unfortunately, we did not succeed. This is due to the fact that what landed on our stack was a query without information about the id. Now we will modify the above code a bit.
Consolidating knowledge is the basis, so I describe the order once again.
- before getComments
- inside getId
- id in getComments function
- after getComments
- all comments
What can we do to properly get data about comments? There are few solutions but the newest / most popular is using async / await.
Simple and pleasant right? The only thing we did was add two keywords - async / await. However, what really happened? Here we will have to enter the territory called Promise in Javascript.
What are the Promises? First of all, these are objects, and secondly, they are quite specific objects.
Most importantly, they are simply promises that occur in a similar form as in everyday life. Each of us has promised something at some point of our lives. To the parents, that we will clean the room, the employer, that we will be on time in the office. Every promise has two stages. The first stage is the stage that I like to call transient. It looks like we made a promise that we'll take out the trash before the end of the day. Since we still have time, our promise is in the state
that is waiting for the final result. In this case, the value will be undefined. The next stage will tell us whether we were able to take out this unfortunate trash before the end of the day or not. If yes and our mission has been successful, then the status will be easy to guess -.
Here, for example, the value is simply an empty string. If, however, we have forgotten and we have failed to fulfill the promise, then the status will be (also an empty string used).
Depending on the status, different things can happen. Let's start with a simple example.
"What the hell is going on here" - you might ask? Well, through new Promise, we create a new object that accepts the callback function with two parameters - resolve and reject, which we later use depending on whether we are above or below 18 years. As you can easily guess, the resolve will serve us to handle the variant when we fulfill the promise and reject when we do not fulfill this promise. It seems pretty simple, right? Now let's move on. If you promise something then ... Well, then what? Here the keyword "then ()" sneaks in. Promises that are fulfilled will be a resolved promise object. "then ()" takes this value and uses a callback function to process it in some way. Let's modify the code a bit and then use then ().
Look. In then() we used a callback with the msg parameter. Because we already know that the promise will be fulfilled, msg will accept the value of the argument we have in resolve (). In our case, it will be a string - "I am old enough to ...". Thanks to that we will get
Now let's see what happens when we change the age in a variable to less than 18, let's say 17.
We got a mistake. This is because then() is used to handle your resolved promises. If, on the other hand, we want to catch a promise that we have not been able to fulfill (was rejected), we will use catch(). So let's add catch() to the code and keep the age of 17.
It will look like this. Of course, the principle is the same. In the error parameter, we get an argument but this time with reject() string "What a shame". Yes, the result will be:
As I suggested. Everything is fairly simple and transparent. Ok, let's add one promise..
As you can see, we have created the order function that will return us a Promise. It's the same Promise as the previous one, the only difference is that it's always resolved in this case, so its value is an argument in resolve(). But what more interesting is what is at the bottom. In the 17th line, we called the function order. Thanks to the fact that in then() we return Promise, we can use again then() (at the end we return the promise), but this time the result of fulfilling the promise will be the argument given in resolve (), which is in the function order.
So what about all of this? What does this give us? Well, thanks to this, we can chain Promises and pass the results from previous Promises to the next ones. Also, the result will always be passed (resolved or rejected Promises) which is very helpful in communicating with external APIs. I will modify the previous code a bit, add some functions that return Promises and chain them.
By capturing values in the parameters of callback functions, we can pass everything down the line. Let me also mention that it is worth remembering that we should always use catch() to be sure when something unexpectedly fails. Thanks to using Promises we do not have to call catch() for each function then(). It's enough to add 1 catch() at the end of any chain of Promises.
Ok, after this brief description of Promises, let's go back to async / await thanks to which we were able to firstly get the post's id and then use it to fetch further information from the API. So what does async / await do? Let's go back to the previous example.
Why did it work? We have received the desired result because await stops the execution of the further part of the function until the moment we get the response. For this reason, only when the function getId is able to return the id, console.log from line 11 and everything that is in the function will be completed. This is a very important thing. We should only use it when we know that action would block the main javascript thread, which would prevent the user from interacting with the application. Okay, but what else does it give us? Well, async / await introduces several facilities that make the code more readable.
Here we have an earlier code with some changes. We removed all then() and the code looks somehow nicer, right? Using await, we not only stopped further execution of the function's code but also obtained immediate access to the answer that Promise returns from fetch.
As you can see, I did as I promised. I tried to include all relevant information in one post. Of course, you could add a bit more to it but in the end, I think that if someone as a novice developer would like to learn more about how the asynchrony works in Javascript, then he has almost all of the needed information here.
Top comments (3)
A great article to read and discuss! I have read through it but there is a part I don't quite understand:
Am I right to assume the bold word "stack" is supposed to be replaced with "queue"?
Many thanks!
Yes, You are totally right, sorry for a mistype. It's also worth noting that, the first thing 'completed' is pushed out of the task queue.
Thanks!