The Javascript Event Loop

In a recent job interview, I was asked to describe the Javascript event loop. It’s pretty amazing, with all the frameworks, libraries, packages, etc available today, that you can often spend a lot of time in development without having to dig into some of the lower-level nuances of a language. I didn’t have a solid understanding of the event loop, so my answer went something like this:

An event is fired, like user input, that changes the data in your application. This signals for your logic to run, causing a diff in the DOM. This diff is then implemented by the browser, completing the loop.

After doing some quick research, I discovered that was definitely not the answer they were looking for. So here is a brief dive into the Javascript event loop.


To understand how Javascript implements the event loop, we need to understand a few important concepts:

  1. Call Stack - The call stack tracks the execution of the code in steps called “frames”.
  1. Frame - A frame is an isolated context of each function call in the stack. It includes local variables and arguments.
  1. Queue - A queue is a series of “messages” (a.k.a. “tasks” - essentially event listeners) that are waiting to processed by the stack.
  1. Message/Task - This is typically a callback that is performed as the result of some form of asynchronous action. “Message” and “task” are typically interchangeable and refer to the same thing.

When something triggers a “task” (user input, a timeout or interval, event callbacks, etc), that task will get added to the queue. A queue will process messages in a FIFO (first-in-first-out) method (with a couple notable exceptions covered below).

If the stack is empty, the oldest task in the queue will get put onto the stack as a “frame”. A stack processes frames in a FILO (first-in-last-out) method. As the frame is processed, any additional functions that are called will be added onto the stack as a frame. Once that frame is finished processing, it is removed off of the stack and the previous frame continues to be processed. Once the original frame is fully processed and removed from the stack, the next message in the queue is added and processed. And so the loop continues.

For a typical web request, the queue and stack are empty at the beginning of the response. As the browser encounters scripts, they are added to the queue and begin processing.

👉🏻Depending on if a script is synchronous, asynchronous, or deferred, it may be handled immediately by the stack or added to the queue, the nuances of which are beyond the scope of this article.

Let’s say we have the following code:

function sayHello(name) {
	console.log(`Hello, ${name}`);
}

function greetPeople() {
	const names = [
		'Patrick',
		'Duane',
		'Lisa',
	];
	
	for (let name of names) {
		sayHello(name);
	}
}

greetPeople();

  1. The script is added to the stack and begins processing.
  1. When the script reaches the greetPeople call, a second frame is added to the stack for the local context of that function. That frame begins processing and the script frame pauses, waiting for the greetPeople frame to complete.
  1. The array of names begins looping in greetPeople and the first name, “Patrick”, is passed to sayHello.
  1. sayHello is added as a new frame above the greetPeople with “Patrick” as it’s argument and begins processing. At this point, the script frame is at the bottom of the stack, the greetPeople frame is above that, and the sayHello frame is on the top. Along with the script frame, the greetPeople frame has also paused processing until the new sayHello frame completes.
  1. Once the new sayHello frame completes, it is removed off the top of the stack, leaving the greetPeople frame at the top of the stack. The greetPeople frame resumes processing. The loop finishes its first iteration, and starts again with “Duane”.
  1. sayHello is added as a new frame above the greetPeople frame again, but this time with “Duane” as it’s argument. It begins processing. Again, the script frame is at the bottom of the stack, the greetPeople frame above it, and sayHello on top.
  1. When this second sayHello frame completes, like the first, it is removed off the top of the stack. The greetPeople frame resumes processing and continues the loop with “Lisa”.
  1. A third sayHello frame is added, processed, and released again.
  1. The greetPeople has completed all iterations of its for loop. It completes processing and is removed from the top of the stack, allowing the script frame to resume.
  1. The script frame also finishes processing and is removed from the stack and the stack is now empty.

You can see the whole process in play here:


In Javascript, there are two different types of messages that the queue handles:

  • Macrotasks (or just Tasks) - These are scripts that are run, user input, asynchronous events, and timing callbacks (setTimeout, setInterval). Tasks are processed first, but any tasks that are added to the queue during the current iteration, will be processed during the next iteration over the queue
  • Microtasks - These include promises, MutationObserver callbacks, and queueMicrotask callbacks. These are processed after all macrotasks during the current iteration. Additionally, any microtasks that are queued will also run during the current iteration. This allows you to guarantee that a portion of your code will run before the next iteration of the queue

A good way to show how this works is with a simple example:

console.log('Start');

setTimeout(() => console.log('Timeout Callback'), 0);

Promise.resolve().then(() => console.log('Promise Callback'));

console.log('End');

Here is how the event loop handles that code:

  1. console.log('Start'); processes immediately because it is part of the execution stack in the current queue iteration.
  1. The setTimeout callback is a macrotask and is, therefore, queued for the next iteration of the event loop.
  1. The promise is a micro task and is added to the queue at the end of the current iteration.
  1. console.log('End'); is processed immediately because it is part of the execution stack in the current queue iteration.
  1. The macrotasks, at this point, are finished processing and the queue moves on to the microtasks.
  1. console.log('Promise Callback') is processed since it is a microtask and runs before the next event loop iteration.
  1. The macrotasks, at this point, are finished processing and the queue is now empty. It begins the next iteration
  1. console.log('Timeout Callback') is processed as part of the second iteration of the queue.


Further Reading:

The event loop - JavaScript | MDN The event loop - JavaScript | MDN

In depth: Microtasks and the JavaScript runtime environment - Web APIs | MDN In depth: Microtasks and the JavaScript runtime environment - Web APIs | MDN

© 2025 Patrick Stephan. All rights reserved.