DEV Community

Rajat Oberoi
Rajat Oberoi

Posted on • Edited on

Understanding the Event Loop, Callback Queue, and Call Stack & Micro Task Queue in JavaScript

Call Stack: Simple Data structure provided by the V8 Engine

  • JavaScript executes code using a Call Stack, which tracks the currently executing function.
  • Main function (the entire script) is automatically added to the call stack.
  • Functions are added to the call stack when called and removed once executed.
  • JavaScript is single-threaded, meaning only one task is processed in the call stack at a time.
//Basic Example

const x = 1;
const y = x + 2;
console.log('Sum is', y);

/*

- This code gets wrapped in main() and main is added to Call Stack.
- log('Sum is 3') added to call stack.
- On console we would get 'Sum is 3'. Now log function is finished and gets removed from Call Stack.
- Now end of script, main function gets popped out of Call Stack.
*/
Enter fullscreen mode Exit fullscreen mode

const listLocations = (locations) => {
    locations.forEach((location) => {
        console.log(location);
    });
}

const myLocation = ['Delhi', 'Punjab'];
listLocations(myLocation)

Enter fullscreen mode Exit fullscreen mode
  1. Main function gets pushed onto the call stack.
  2. Line 1 we are declaring the function but not calling it, hence it will not get added to call stack.
  3. Line 7 we are defining our location array.
  4. Line 8 Function call, So it is going to be pushed to call stack and is the top item there.
  5. listLocations will start running. pushed to call stack.
  6. forEach is a function call so gets added to call stack. forEach calls anonymus function one time for each location.
  7. anonymous('Delhi) function gets added to call stack with argument Delhi.
  8. Now console.log function gets added to call stack. It prints Delhi, and finishes. and pops out.
  9. anonymous('Delhi) finishes and pops out.
  10. forEach is not done yet hence does not pops out. anonymous('Punjab) gets added to call stack.
  11. Now console.log function gets added to call stack. It prints Punjab, and finishes. and pops out.
  12. forEach is completed and hence poped out of call stack.
  13. listLocations is done, hence pops out.
  14. Script is completed. main() pops out.

Callback Queue

It's job is to maintain a list of all of the callback functions that needs to be executed.

console.log('Starting Up!');

setTimeout(() => {
    console.log('Two Seconds!');
}, 2000);

setTimeout(() => {
    console.log('Zero Seconds!');
}, 0);

console.log('Finishing Up!');

Enter fullscreen mode Exit fullscreen mode
  1. main() pushed to call stack.
  2. Line 3: setTimeout pushed to call stack. < setTimeout is not part of JS V8 but is part of NodeJS. It's implementation is in C++ provided by NodeJs.
  3. setTimeout when called registers an event which is an event-callback pair. Event here is wait 2 seconds and callback is the function to run. Another example of event-callback pair is wait for database request to complete and then run the callback that does something with the data.
  4. This new event i.e. setTimeout function is popped and is registered in Node APIs. 2 Seconds clock starts ticking down.
    While waiting for those 2 seconds we can do other stuff < Non Blocking nature of node >

  5. Line 7: setTimeout registers another event in Node API.

  6. Now timeout 0 seconds are up, now the callback needs to be executed.

Callback Queue comes in picture: It's job is to maintain a list of all of the callback functions that needs to be executed. Front item gets executed first.

  1. callback of setTimeout with 0 seconds timeout gets added to queue so that it can be executed. But to get executed it needs to be added on Call Stack, that's where function go to run.

Now, here Event Loops comes in picture, it looks at the call stack and callback queue, If call stack is empty then it will run items from callback queue. < This is the reason 'Finishing Up!' logged before 'Zero Seconds!' as main was in call stack, event loop is waiting for main to get popped out>

  1. log('Zero Seconds!') gets added to call stack. and message is printed on console.
  2. main is completed and pops out.
  3. Event loop takes item from call back queue and push to call stack. 'Zero Seconds!' prints.
  4. Once 2 seconds achieved, callback('Two Seconds!') added to callback queue, moves to call stack, gets executed. 'Two Seconds!' prints.
  • The delay specified in setTimeout is not the exact timing of execution but rather the minimum delay after which the callback can be added to the callback queue.
  • The actual execution time depends on the event loop's scheduling and the availability of the call stack. This asynchronous behaviour allows JavaScript to handle non-blocking operations effectively, especially in environments like Node.js where I/O operations are common.

Non-Blocking Nature of Node.js

  • Though JavaScript is single-threaded, meaning only one function can be executed at a time.
  • it achieves non-blocking behavior using Node APIs for asynchronous tasks like setTimeout, database calls, etc.
  • While the call stack is executing synchronous code, the environment handles asynchronous tasks in the background.

Micro Task Queue

  • When working with Promise, NodeJS works with micro task queue.
  • Microtasks are queued for execution.
  • When a Promise is resolved or rejected, its .then() or .catch() callbacks are added to the microtask queue.
  • In async await: When await is used inside an async function, it essentially breaks the function into two parts:

  • Synchronous Part: The part before the await keyword executes synchronously.

  • Asynchronous Part: The part after await executes asynchronously once the awaited promise resolves.

  • Microtasks come into play when promises are resolved inside async functions using await. After the awaited promise resolves, the callback (or subsequent async code) following the await is placed in the Microtask Queue for execution.

  • Event Loop prioritise the microtask queue. Microtasks have higher priority than macrotasks (such as setTimeout callbacks or event handlers), which means they are executed as soon as the call stack is empty and before the event loop moves to the next macrotask.

  • First micro task queue is emptied then event loop moves to callback queue.

  • After each task picked from callback queue and pushed to call stack, event loop will check micro task queue.

Structured summary:

Call Stack:

  • JavaScript executes code using a Call Stack, which tracks the currently executing function.
  • Main function (the entire script) is automatically added to the call stack.
  • Functions are added to the call stack when called and removed once executed.
  • JavaScript is single-threaded, meaning only one task is processed in the call stack at a time.

Asynchronous Behavior:

  • Though JavaScript is single-threaded, it achieves non-blocking behavior using Node APIs for asynchronous tasks like setTimeout, database calls, etc.
  • These APIs handle long-running tasks in the background, allowing other synchronous code to execute.

Callback Queue:

  • After an asynchronous task is complete, its callback is added to the Callback Queue.
  • The Event Loop checks whether the Call Stack is empty and, if so, pushes callbacks from the queue to the call stack.
  • Example: setTimeout(() => console.log('Hello'), 0) will be executed after the synchronous code finishes, despite the 0ms timeout.

Event Loop:

  • Event Loop continuously checks if the call stack is empty.
  • It then processes tasks from the Callback Queue or Microtask Queue (like Promise callbacks).

Microtask Queue:

  • Microtasks are usually created by promises.
  • When a promise is resolved or rejected, the corresponding .then() or .catch() handler is added to the Microtask Queue.
  • Microtasks have higher priority than Macrotasks (like setTimeout callbacks).
  • Before handling any macrotasks from the callback queue, the event loop checks and clears the Microtask Queue.

Execution Order:

  • Promises and async functions utilize the microtask queue for their resolution/rejection handlers.
  • Example:
  • Promises' .then() callbacks run before setTimeout callbacks, even if the timeout is 0ms.

Summary of Execution Order:

  • Synchronous code executes first (added to the call stack).
  • Once the call stack is empty, Microtasks (e.g., resolved promises) are handled.
  • After the microtask queue is cleared, Macrotasks (e.g., setTimeout, I/O callbacks) are processed from the Callback Queue.
  • This explains why promises' .then() callbacks are executed before setTimeout callbacks, even with a 0ms timeout, and how JavaScript manages non-blocking operations effectively.

Top comments (0)