DEV Community

Cover image for Asynchronous JavaScript - How I understand it.
Alexander Oguejiofor
Alexander Oguejiofor

Posted on • Updated on

Asynchronous JavaScript - How I understand it.

JavaScript is a single threaded language, which means that one command runs at time. It is also synchronously executed, making each command run in the order the code appears.

So imagine we have a task that in our application which accesses a server to get data, and this process takes a long time. What if we have code that we need to run that displays the data response from the server? This poses a challenge, we want to wait for data from the server so that it is there to be displayed, but no code can be run in the meantime.

Enter Asynchronous JavaScript, the feature that makes dynamic web apps possible. We know that the JavaScript engine has three main parts - The thread of execution, Memory (Heap) and the Call stack. However, these aren’t enough as JavaScript needs other external pieces from the Browser like the Console, Timer, Sockets, Network requests and the HTML DOM to function the way we want it to.

JavaScript lets us interact with these tools by providing us with a bunch of functions (Web APIs) like FETCH for Network Requests, Document for the HTML DOM, setTimeout for the Timer and Console for the Console.

The FETCH API is two pronged, ergo it not only initiates the task in the web browser to make a network request to the address passed into it. It also has a consequence in JavaScript which returns a placeholder object called a Promise.

What is a promise?

A promise in JavaScript is very much like a promise in real life. For example, if you make a promise in real life to visit a friend, this promise has two possible results. It is either fulfilled and resolved, or failed and rejected. What this means is that if you go visit your friend, the promise has been fulfilled and resolved, but if you don’t, the promise will be rejected because you were unable to fulfil the promise.

In JavaScript, a promise is an object with three properties, Values, onFulfilled and onRejected. This promise will produce a value in future: a resolved value, or a reason why it is not resolved (e.g., if a network error occurs).

We’ll see an example of how promises work using some code, but before we start we need to define some concepts that will help us along the way.

Event Loop - This is responsible for executing the code, collecting and processing events, and executing queued sub-tasks.

Callback queue - This is where your asynchronous code gets pushed to wait for the event loop to push it into the call stack for execution.

Micro-task queue - Like the callback queue, but has a higher priority which means the event loop checks to see that the micro task queue is empty before moving on to the callback queue.

Execution context - This basically holds all the information about the environment within which the current code is being executed.

const display = (response) => {console.log(response)}
const sayHi = () => {console.log(`say Hi`)}
const runFor300ms = () => { 
   // code that will run for 300ms
}
setTimeout(sayHi, 0)
const futureDisplay = fetch(`https://someserver.com/data/alex/1`)
futureDisplay.then(display)
runFor300ms()
console.log(`I am first`) 
Enter fullscreen mode Exit fullscreen mode

Going through the code snippet above synchronously like the JavaScript engine would:

const display = (response) => {console.log(response)}
Enter fullscreen mode Exit fullscreen mode

First, declare and store the function display in global memory.

const sayHi = () => {console.log(`say Hi`)}
Enter fullscreen mode Exit fullscreen mode

We declare and store the function sayHi in global memory.

const runFor300ms = () => { 
   // code that will run for 300ms
}
Enter fullscreen mode Exit fullscreen mode

On line three, we also declare and store the function runFor300ms in global memory.

setTimeout(sayHi, 0)
Enter fullscreen mode Exit fullscreen mode

The setTimeout( ) method is called, and it triggers the timer in the web browser to execute the function sayHi at 0ms, which is when the timer is set to expire. At exactly 0ms, which is immediately, the sayHi function is pushed into the callback queue where it waits for the call stack to be empty and the global thread of execution complete.

const futureDisplay = fetch(`https://someserver.com/data/alex/1`)
Enter fullscreen mode Exit fullscreen mode

Next, at say 1ms, the constant futureDisplay is declared in global memory and its value is the evaluation of FETCH which is a WEB API that returns a promise object to be stored immediately in futureDisplay. This object will have three properties, Value, which will be set to undefined, onFulfilled and onRejected which will both be empty arrays. In the web browser, the FETCH function will also trigger a network request to get data from the address passed to it. Whenever this response gets back, the data will be stored in the Value property of the promise object, replacing its previous ‘undefined’ value.

futureDisplay.then(display)
Enter fullscreen mode Exit fullscreen mode

On the next line, the request is sent to the address. The data response can come back at anytime, so we need JavaScript to somehow automatically use the data when it is returned. This is where the onFulfilled property on the promise object comes in. We can slide a function into the onFulfilled array, and when the value property is filled, the function is executed with the contents of the value property as input. The .then method is what is used to push the display function into the onFulfilled property on the promise object.

runFor300ms()
Enter fullscreen mode Exit fullscreen mode
  1. At 2ms, we execute the function runFor300ms, create a brand new execution context, and push the function into the call stack. This block of code could be a for loop of some sort that will run for 300ms. Meanwhile, at say 250ms, the network request that was triggered as a result of calling the FETCH function resolves and responds with a string ‘Hello’. This string will replace ‘undefined’ as futureDisplay’s value property. The function display will be returned from the onFulfilled object and stored in the micro-task queue where it will wait to be executed.
  2. runFor300ms( ) is done executing and is popped off the call stack.
console.log(`I am first`) 
Enter fullscreen mode Exit fullscreen mode
  1. The last line executes and the console logs ‘I am First’.
  2. At say 303ms, the Event Loop checks that the call stack and the global execution context are empty. The micro-task queue has priority over the callback queue, so the Event Loop checks it to see if anything needs to be run. It finds the display function sitting pretty there waiting to be run, and pushes it into the call stack to be executed. The function executes and the string ‘Hello’ is printed.
  3. The event loop then checks the callback queue, where it finds sayHi waiting patiently. It pushes it into the call stack to be executed. Upon execution, it prints ‘Hi’.
  4. Our output will be in the order
'I am First'
'Hello'
'Hi'
Enter fullscreen mode Exit fullscreen mode

Conclusion

Remember the promise object has an onRejected property which is also an empty array? Any function stored in this array will be run if the network request fails. This is used for error handling, and functions are pushed into this array using the .catch method.

Promises, WEB APIs, the event loop, micro task and callback queues constitute Asynchronous JavaScript which is the spine of the modern web. It provides us with functionality that allows us not have to wait in a single thread and block further code from running.

Top comments (4)

Collapse
 
szilardszabo profile image
SS • Edited

Excellent and very very helpful article. I really really like the way that it clearly explains confusing topics and hidden, really less known aspects of Javascript.
I just want to make one small clarification about the terminology. I understand that it was not worded in this way as it might just confuses less educated developers but what is single threaded is not the Javascript language itself but most of the currently used Javascript engines that execute the code. More precisely threads are managed by the engine. So for example the V8 engine provides only a single stack and memory heap for your code, and executes it in a single thread, but it also manages other threads to support the execution on which you have no control over. So Javascript actually runs in a multi-threaded way by the engine, from which you only have 1 thread to control and use. But it is absolutely possible to create a different execution environment or call it "engine", that would run Javascript in a different way and use multiple threads for you Javascript code, especially if you run Javascript outside of a browser, e.g. as a standalone application If you google it, then you can even find examples of these.
It is absolutely true however that in most environment, especially in a browser your Javascript code runs as a single thread, so I don't want to confuse people about this fact.

Collapse
 
master_elodin profile image
Alexander Oguejiofor

Aha! Thanks for pointing that out.

Collapse
 
simseb profile image
simseb

Excellent

Collapse
 
ajayadav09 profile image
ajayadav09

Thank you gave some clarity.