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);

In this example, the sequence of function calls and how they are managed in the call stack is as follows:
- Global Execution Context: The JavaScript engine creates a global execution context (
main()
) and pushes it onto the call stack. - printTotal(4, 5): The engine executes the call to
printTotal(4, 5)
, creates a function execution context forprintTotal
, and pushes it to the top of the call stack. - calculate(4, 5): Inside
printTotal
, thecalculate(4, 5)
function is called. The engine creates a function execution context forcalculate
and pushes it to the top of the call stack. - console.log(“calculate is called”): The
console.log("calculate is called")
statement withincalculate
is pushed onto the call stack. Since it is just a print statement, once executed, it is removed from the call stack. - add(4, 5): The
calculate
function callsadd(4, 5)
. The engine creates a function execution context foradd
and pushes it to the top of the call stack. - console.log(“add is called”): The
console.log("add is called")
statement withinadd
is pushed onto the call stack. After it prints to the console, it is removed from the call stack. - return a + b: The
add
function encounters thereturn
statement, calculates the sum, and then theadd
function execution context is popped off the call stack. - calculate(4, 5): The
calculate
function receives the result fromadd
, returns the result, and then thecalculate
function execution context is popped off the call stack. - console.log(total): Back in
printTotal
, theconsole.log(total)
statement is pushed onto the call stack. After printing the total, it is removed from the call stack. - 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.

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");

When we run the code, the following sequence of events occurs:
Execution Steps
- 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.
- Execution of
setTimeout(cb, 2000)
:setTimeout
with the callbackcb
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.
- 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.
- 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.
- After 2 seconds, the timer finishes, and the callback
- 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.
- 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 bycb()
itself.
- The call stack is now empty, so the callback
This process ensures that asynchronous tasks like setTimeout
do not block the main thread, allowing for a smooth and responsive user experience.

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");

When we run the code, the following sequence of events occurs:
console.log("Number 1")
:- This is pushed to the call stack and executed immediately. It pops off the call stack afterward.
setTimeout
:- The
setTimeout
function is called, which pushes the callbackcb
to the call stack. The timer starts in the Web APIs environment, and once initiated,setTimeout
pops off the call stack.
- The
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.
- The
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 fromsetTimeout
.
Event Loop Behavior
- 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.
- 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
- Executing
then()
Callback:console.log("Number 4 - Promise")
is executed, pushed onto the call stack, printed, and then popped off.
- 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.
- Executing
cb()
:- The
cb()
callback fromsetTimeout
is now pushed to the call stack.console.log("Number 2")
is executed, printed, and then popped off.
- The
- Final State:
- The
cb()
function itself is then popped off the call stack, completing the execution.
- The

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