Top Interview Questions on Asynchronous JavaScript

Top Interview Questions on Asynchronous JavaScript

This tutorial covered the basics of asynchronous JavaScript, including important concepts like the event loop, callback queues, microtasks, and Web APIs. We know JavaScript is a single-threaded programming language, meaning it has a single call stack. When executing JavaScript code, it runs one thing at a time.

Let’s understand how the JavaScript engine executes code and the concept of the call stack. The call stack is used to manage execution contexts. When a code block or function is called, it is first pushed onto the call stack. Once the code has been executed or the function has returned, it is popped off the top of the stack.

The Call Stack

When the JavaScript engine executes the code, it follows a specific sequence of steps using the call stack. Let’s break down the steps for our example:

function add(a, b) {
    console.log("add is called");
    return a + b;
}

function calculate(a, b) {
    console.log("calculate is called");
    return add(a, b);
}

function printTotal(a, b) {
    let total = calculate(a, b);
    console.log(total);
}

printTotal(4, 5);
image Top Interview Questions on Asynchronous JavaScript

In this example, the sequence of function calls and how they are managed in the call stack is as follows:

  1. Global Execution Context: The JavaScript engine creates a global execution context (main()) and pushes it onto the call stack.
  2. printTotal(4, 5): The engine executes the call to printTotal(4, 5), creates a function execution context for printTotal, and pushes it to the top of the call stack.
  3. calculate(4, 5): Inside printTotal, the calculate(4, 5) function is called. The engine creates a function execution context for calculate and pushes it to the top of the call stack.
  4. console.log(“calculate is called”): The console.log("calculate is called") statement within calculate is pushed onto the call stack. Since it is just a print statement, once executed, it is removed from the call stack.
  5. add(4, 5): The calculate function calls add(4, 5). The engine creates a function execution context for add and pushes it to the top of the call stack.
  6. console.log(“add is called”): The console.log("add is called") statement within add is pushed onto the call stack. After it prints to the console, it is removed from the call stack.
  7. return a + b: The add function encounters the return statement, calculates the sum, and then the add function execution context is popped off the call stack.
  8. calculate(4, 5): The calculate function receives the result from add, returns the result, and then the calculate function execution context is popped off the call stack.
  9. console.log(total): Back in printTotal, the console.log(total) statement is pushed onto the call stack. After printing the total, it is removed from the call stack.
  10. printTotal(4, 5): Finally, the printTotal function execution context is popped off the call stack, and the execution returns to the global context.

This step-by-step process ensures that functions are executed in the correct order, maintaining an organized and predictable flow of execution.

image-1 Top Interview Questions on Asynchronous JavaScript

The Asynchronous Callbacks

Running long operations in JavaScript can be problematic, especially when dealing with tasks like API calls. For example, when the front end calls an API to retrieve data and render it on the web browser, the API call might take some time to process. During this time, if the main thread is blocked, the web browser becomes unresponsive, making it unfriendly for users who can’t interact with the web page until the request is complete.

Asynchronous callbacks are the solution. They allow us to create fluid UIs without blocking the browser.

Although we refer to these operations as asynchronous, it doesn’t mean JavaScript is running multi-threaded. As mentioned earlier, JavaScript is single-threaded. So, how does JavaScript achieve asynchrony?

The web browser has components such as the callback queue, event loop, and web APIs to support activities that run concurrently and asynchronously. These components work together to handle asynchronous operations without blocking the main thread:

Web APIs

Web APIs like setTimeout, setInterval, setImmediate, DOM events, and AJAX requests are provided by the browser in the global object. We can call these APIs directly in JavaScript code. The browser runtime handles the API requests to prevent them from blocking the call stack.

setTimeout(() => {
    console.log("Hello World");
}, 2000);

When setTimeout is invoked, the callback function is tracked by Web APIs until the timeout completes after 2 seconds. Instead of executing the callback function immediately, it is pushed to the callback queue.

Callback Queue

The callback passed to the function in Web APIs is pushed into the callback queue to wait for its turn to be invoked in the call stack. The callback function in the callback queue must wait until the call stack is empty before it can be executed.

Event Loop

The event loop constantly monitors both the callback queue and the call stack. Once the call stack is empty, the event loop takes the callback from the queue and pushes it to the call stack to be executed.

Let’s take a look at an example to better understand how asynchronous execution works:

console.log("Number 1");

setTimeout(function cb() {
    console.log("Number 2");
}, 2000);

console.log("Number 3");
image-2 Top Interview Questions on Asynchronous JavaScript

When we run the code, the following sequence of events occurs:

Execution Steps

  1. Execution of console.log("Number 1"):
    • console.log("Number 1") is pushed to the top of the call stack and executed immediately.
    • After execution, it pops off the call stack.
  2. Execution of setTimeout(cb, 2000):
    • setTimeout with the callback cb and a delay of 2 seconds is called and pushed to the call stack.
    • Since setTimeout is a Web API provided by the browser and not part of the V8 runtime, the timer starts in the Web APIs environment.
    • The setTimeout function itself completes and pops off the call stack.
  3. Execution of console.log("Number 3"):
    • console.log("Number 3") is pushed to the top of the call stack and executed immediately.
    • After execution, it pops off the call stack.
  4. Callback cb() in the Task Queue:
    • After 2 seconds, the timer finishes, and the callback cb() function is pushed to the task queue (also known as the macrotask queue).
    • The callback cb function does not execute immediately; it waits in the task queue.
  5. Event Loop:
    • The event loop continuously monitors both the task queue and the call stack.
    • Once the call stack is empty, the event loop takes the first task from the task queue and pushes it onto the call stack.
  6. Execution of cb():
    • The call stack is now empty, so the callback cb() function is pushed to the call stack for execution.
    • Inside cb, console.log("Number 2") is pushed to the top of the call stack and executed.
    • After execution, console.log("Number 2") pops off the call stack, followed by cb() itself.

This process ensures that asynchronous tasks like setTimeout do not block the main thread, allowing for a smooth and responsive user experience.

callback Top Interview Questions on Asynchronous JavaScript

Microtasks

Microtasks play a crucial role in JavaScript’s asynchronous behavior, particularly with constructs like Promises and async/await. These operations use the job queue (microtask queue) to run their callbacks, which have a higher priority than those in the task queue (macrotask queue). The event loop always processes microtasks first before moving on to the task queue.

In ES6, Promises were introduced to help manage asynchronous code and avoid “callback hell.” In this tutorial, we will use Promises to demonstrate how microtask queues work.

console.log("Number 1");

setTimeout(function cb() {
    console.log("Number 2");
}, 2000);

Promise.resolve()
    .then(function() {
        console.log("Number 4 - Promise");
    });

console.log("Number 5");
image-4 Top Interview Questions on Asynchronous JavaScript

When we run the code, the following sequence of events occurs:

  1. console.log("Number 1"):
    • This is pushed to the call stack and executed immediately. It pops off the call stack afterward.
  2. setTimeout:
    • The setTimeout function is called, which pushes the callback cb to the call stack. The timer starts in the Web APIs environment, and once initiated, setTimeout pops off the call stack.
  3. Promise.resolve():
    • The Promise.resolve() call is pushed onto the call stack. Once the promise is resolved, the .then() callback is added to the microtask queue.
  4. console.log("Number 5"):
    • This statement is executed next, pushed to the call stack, printed to the console, and then popped off.

Queue State

At this point, both the microtask queue and the macrotask queue each have one task waiting:

  • Microtask Queue: Contains the then() callback.
  • Macrotask Queue: Contains the cb() callback from setTimeout.

Event Loop Behavior

  1. Checking Microtask Queue:
    • The event loop detects that the call stack is empty. It first checks the microtask queue. Since there’s a task waiting, it pushes the then() callback onto the call stack.
  2. Executing then() Callback:
    • console.log("Number 4 - Promise") is executed, pushed onto the call stack, printed, and then popped off.
  3. Empty Call Stack Check:
    • The event loop checks the call stack again. It finds it empty and checks the microtask queue again. Since there are no unprocessed callbacks left, it proceeds to the macrotask queue.
  4. Executing cb():
    • The cb() callback from setTimeout is now pushed to the call stack. console.log("Number 2") is executed, printed, and then popped off.
  5. Final State:
    • The cb() function itself is then popped off the call stack, completing the execution.
microtask Top Interview Questions on Asynchronous JavaScript

This sequence of operations illustrates how microtasks (from the job queue) are prioritized over macrotasks (from the task queue) in JavaScript’s event loop. This mechanism ensures that Promise callbacks are executed as soon as possible, leading to more predictable and responsive asynchronous behaviour.

What happens when you set setTimeout to 0 milliseconds, and how does it affect the execution order?

The process remains the same. This behaviour is consistent across all Web APIs, including AJAX requests and DOM event listeners. They are first handled by the Web APIs environment. Once completed, their callbacks are pushed to the queue and picked up by the event loop when the call stack is free. This ensures non-blocking execution, providing a smooth user experience.

console.log("Number 1");

setTimeout(function cb() {
    console.log("Number 2");
}, 0);

console.log("Number 3");

Number 1
Number 3
Number 2

If you have multiple setTimeout(fn, 0) calls in a row, how will they be executed in relation to other synchronous code?

When you have multiple setTimeout(fn, 0) calls in a row, they will all be queued in the macrotask queue but executed only after the call stack is empty. All setTimeout(fn, 0) calls will execute in the order they were called after all synchronous code has been completed, demonstrating the nature of the macrotask queue in the JavaScript event loop.

console.log("Start");

setTimeout(() => console.log("Timeout 1"), 0);
setTimeout(() => console.log("Timeout 2"), 0);
setTimeout(() => console.log("Timeout 3"), 0);

console.log("End");

Start
End
Timeout 1
Timeout 2
Timeout 3

Conclusion

Understanding asynchronous behaviour in JavaScript is important for creating responsive applications. By learning about the event loop, callback queues, microtasks, and Web APIs, we can handle tasks without freezing the main thread. This ensures that users have a smooth experience.

Share this content:

Leave a Comment

Discover more from nnyw@tech

Subscribe now to keep reading and get access to the full archive.

Continue reading