Asynchronous Javascript and Some advanced use cases

mdimran1409036

Md. Imran Hossain

Posted on October 25, 2023

Asynchronous Javascript and Some advanced use cases

Although Javascript is a single-threaded programming language and it executes programs synchronously, It also handles asynchronous tasks using Web API, Task queue and event loop. Whenever an asynchronous code block comes to the call stack following things happen:

  • It gets deferred to Web API
  • Web API tries to resolve the code, and whenever it gets resolved, goes to the Callback Queue
  • Event Loop always watches the call stack
  • The moment the Call Stack gets empty, Event Loop pushes the resolved task to the Call Stack
  • Call Stack executes the code

During this process, after the asynchronous tasks get resolved from Web API, the Callback Queue gets populated with the following tasks:

  1. Macro Tasks (Task Queue)
  2. Micro Tasks (Job Queue)

The Task Queue is responsible for these tasks: setTimeout, setInterval, setImmediate, I/O tasks, etc.
The Job Queue handles these tasks: Promises, processes.nextTick, etc.

As Event Loop takes the resolved tasks from the Callback Queue and the Callback Queue has two task Queues, So the obvious question is how Event Loop decides which queue to dequeue from.

This answer depends on the following rules:

  1. For each loop of the Event loop, one Macro Task is completed out of the Macro Task Queue.
  2. Once that task is complete, the event loop visits the Micro Task Queue. The entire Micro Task Queue is completed before the Event Loop looks into the next thing.
  3. At any point in time, if both the queues got entries, Job Queue(Micro Task Queue) gets higher precedence than Task Queue (Macro Task Queue).

Image description

These are all the magic that happens behind Asynchronous code execution. Now let's move into some examples to validate the above discussion.
Example 1:

console.log("first");

fetch("https://jsonplaceholder.typicode.com/todos/1")
    .then((response) => response.json())
    .then((json) => console.log("json placeholder result", json));

setTimeout(() => {
    console.log("after 1000 millisecond");
}, 500);

setTimeout(() => {
    console.log("after 0 millisecond");
}, 0);

const promise = new Promise((resolve, reject) => {
    resolve("I am promise and I am resolved");
});
promise.then((response) => console.log(response));

console.log("second");
Enter fullscreen mode Exit fullscreen mode

So what do you think is the output of the result?
the output should be like this:

first
second
I am promise and I am resolved
after 0 millisecond
json placeholder result {
    "userId": 1,
    "id": 1,
    "title": "delectus aut autem",
    "completed": false
}
after 1000 millisecond
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. The first line comes to the call stack, it is synchronous so executes immediately and logs "first";
  2. The asynchronous operations (fetch, setTimeout(0 & 1000 milliseconds), and Promise) are deferred to the Web API.
  3. The last line is also synchronous and so it is executed by the Call stack and logs "second". Now the call stack is empty.
  4. Although fetch is called before setTimeout, it takes some time to be resolved as it communicates with external API. so it's still pending in the Web API.
  5. Meanwhile the setTimeout with 0 milliseconds and promise gets resolved immediately. The promise gets pushed to the Micro Task Queue and setTimeout with 0 milliseconds gets pushed to the Macro Task Queue. The Micro Task Queue gets more priority than the Macro Task Queue for both entries. So Event Loop will take the promise first and the setTimeout second. so the log order should be "I am promise and I am resolved" "after 0 millisecond"
  6. At this point Web API resolves(hopefully i.e. depends on the external server "jsonplaceholder") the fetch and goes to Micro Task Queue. Event Loop takes the resolved result and push to Call Stack. Call stack then prints the result.
  7. Lastly, the setTimeout with 1000 milliseconds gets resolved and moves to Macro Task. Event Loop then push it to the call stack and logs "after 1000 millisecond"

Example 2

const p1 = new Promise((resolve, reject)=>{
    setTimeout(()=>{
        resolve("p1")
    },4000)
}) 
const p2=new Promise((resolve, reject)=>{
    setTimeout(()=>{
        resolve("p2")
    },8000)
}) 
const p3= new Promise((resolve, reject)=>{
    setTimeout(()=>{
        resolve("p3")
    },0)
}) 

async function main(){
    console.log(await p1)   
    console.log(await p2) 
    console.log(await p3) 
}
main()
Enter fullscreen mode Exit fullscreen mode

What will be the output and how much time will take for p1, p2 and p3 to be executed?

The answer should be:

p1 - 4 second
p2 - 8 second
p3 - 8 second
Enter fullscreen mode Exit fullscreen mode

Explanation:
when the code starts to get executed:

  1. p1, p2 and p3 will all be deferred to Web API before executing the main() function.
  2. As the new Promise is an instance of the Promise class, the constructor method will already be executed before executing main() function which results as setTimeout with 4000 milliseconds, 8000 milliseconds, and 0 milliseconds all will be in the Web API and will be in the pending state.
  3. Now, the main() function is called which is implemented using async await.
  4. Now, the compiler will try to execute p1 and wait until p1 is resolved from Web API and it will take minimum 4 second
  5. Secondly, the compiler will try to execute p2 and wait until p2 is resolved from Web API. As it was previously in the Web API in pending state it will take 8 seconds from the very beginning.
  6. Thirdly, p3 is already resolved as it has 0 millisecond timeout. But it had to wait until the p2 is finished. p2 took 8 second so it will also take 8 second.

Example 3

const p1 =()=> new Promise((resolve, reject)=>{
    setTimeout(()=>{
        resolve("p1")
    },4000)
}) 
const p2=()=>new Promise((resolve, reject)=>{
    setTimeout(()=>{
        resolve("p2")
    },8000)
}) 
const p3=()=> new Promise((resolve, reject)=>{
    setTimeout(()=>{
        resolve("p3")
    },0)
}) 

async function main(){
    console.log(await p1())   
    console.log(await p2()) 
    console.log(await p3()) 
}
main()
Enter fullscreen mode Exit fullscreen mode

This problem is identical to example 2. But slightly different as p1,p2 and p3 are functions. So what will be the result now?

The answer should be:

p1 - 4 second
p2 - 12 second
p3 - 12 second
Enter fullscreen mode Exit fullscreen mode

Now why the answer is different?
Explanation

  1. The promises are not executed before main() function gets called.
  2. p1,p2, and p3 are just function reference not promise.
  3. When p1() is called inside the main function it waits for 4 seconds to be executed, then p2() gets called.
  4. till now, we have already passed 4 seconds, p2() takes another 8 seconds. So, total 8+4=12 seconds will be taken.
  5. p3() resolves immediately, so the total time taken for p3 is 12+0=12 seconds.

(will be continued...)

💖 💪 🙅 🚩
mdimran1409036
Md. Imran Hossain

Posted on October 25, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related