How does JS work? Event Loop
- Pavol Megela
- Oct 24, 2022
- 6 min read

Understanding the JavaScript event loop is essential for any developer working with modern web technologies. Despite JavaScript’s reputation as a single-threaded language, its ability to handle asynchronous operations efficiently is what makes building responsive, fluid applications possible. In this article, we’ll explore the core concepts behind the event loop, its relation to the call stack, and how browsers manage tasks asynchronously.
The Call Stack
The call stack is a data structure that keeps track of where your code is in its execution. Every time a function is called, a new stack frame is pushed onto the call stack. When that function returns, its frame is removed. This simple mechanism (Last in-First out) means that JavaScript can only execute one function at a time. It also explains why a blocking piece of code (like a heavy loop or a synchronous network request) can halt everything until it’s finished. To help visualise it, refer to the image below.

You already heard about the Call Stack
Many error logs include a stack trace, which is a snapshot of the call stack at the time an error occurred. For instance, if an error is thrown deep inside nested function calls, the error log will show a trace that looks something like this:
Uncaught Error: Something went wrong
at innerFunction (app.js:25:15)
at middleFunction (app.js:20:10)
at outerFunction (app.js:15:5)
Great, now it surely sounds familiar. Another common issue related to the call stack is infinite recursion, that's when you write a piece of code that continuously calls itself without stopping. In JavaScript, this can quickly exhaust the call stack, leading to a “Maximum call stack size exceeded” error, like in the following example:
function recursiveFunction() {
recursiveFunction();
}
recursiveFunction(); // This call will eventually crash the program
This error indicates that the call stack has been filled with too many function calls, causing the program to shut down or crash.
Where does async functionality comes from?
Even though JavaScript’s execution happens on a single thread (meaning only one function can run at a time), the browser is built to perform several operations simultaneously. The secret to this lies in its architecture.
When running JS code in a browser (or Node), it’s not just the Call Stack; there’s also the Web APIs, the Task Queue, the Microtask Queue, and the Event Loop, all working together to manage asynchronous operations and ensure smooth execution of JavaScript code. Let's look at more accurate browser architecture, image below.

Web APIs
Web APIs are provided by the browser (or Node.js, though they are called differently there) to perform operations that would be too time-consuming or blocking if executed directly in JavaScript. These include:
Timers - Functions like setTimeout and setInterval let you schedule code execution after a delay.
HTTP Requests - Methods such as fetch that allow you to perform network operations asynchronously.
DOM Events - Event listeners for user interactions (clicks, key presses, etc.) are handled by browser-provided APIs.
Web APIs operate outside the main JavaScript engine and offload heavy tasks to background threads, ensuring the main thread remains free to process code when callbacks are ready.
Task Queue
The task queue is where asynchronous callbacks are placed once their corresponding Web API has finished its work. This queue holds the tasks until the JavaScript engine’s call stack is empty. To simplify the visualizations, we combine the Task Queue and Microtask Queue, but let's briefly highlight the difference so you have an understanding.
• Macro-task (or task, or callback) queue: This queue holds tasks like callbacks from setTimeout, setInterval, and I/O events.
• Micro-task queue: This queue holds tasks from promises (.then callbacks), process.nextTick (in Node.js), and certain other microtasks.
Think of the task queue as a waiting area where the callbacks line up until they’re ready to be processed by the call stack.
The Event Loop
The event loop is the orchestrator that manages the interaction between the call stack and the task queue. It continuously monitors the call stack to see if it’s empty. When it is, it picks the next callback from the task queue and pushes it onto the call stack for execution.
Continuous Process: This loop runs as long as the application is active.
Prioritization: Besides the task queue, the event loop also handles microtasks (from promises, for example) immediately after the current execution context is cleared but before moving on to the next macro-task.
Simple Example
This code defines a function named main that first logs “Start”, then uses setTimeout to schedule a callback function that will log “Timeout finished” after approx. 1000-millisecond delay, and finally logs “End” before the function is called.
console.log("Start");
setTimeout(() => {
console.log("Timeout finished");
}, 1000);
console.log("End");
How It Will Get Executed
Below is an animation that visually demonstrates the flow of execution in our example.

1. The call to main() pushes its execution context onto the call stack.
2. Inside main(), console.log("Start") is called, so a new frame is pushed onto the stack, immediately executed, printing “Start”, and then popped off the stack.
3. The setTimeout function is called next; its frame is pushed onto the call stack, and it immediately delegates the timer to the browser’s Web API before returning, so its frame is quickly removed.
4. Then, console.log("End") is executed by pushing its frame onto the stack, printing “End”, and subsequently popping it off.
5. Once all synchronous code in main() has run, the call stack becomes empty.
6. After 1000 milliseconds, the timer in the Web API expires and the callback is moved into the Task Queue.
7. The Event Loop detects the empty call stack and moves the callback from the Task Queue onto the call stack.
8. The callback is executed, calling console.log("Timeout finished"), which prints the message, and then its frame is popped off the call stack.
Let's Make It Interesting
Let's look at this example with two consecutive setTimeout calls:
console.log("Start");
setTimeout(() => {
console.log("Timeout 1");
}, 1000);
setTimeout(() => {
console.log("Timeout 2");
}, 1000);
console.log("End");
1. The main() function is pushed onto the call stack.
2. The first setTimeout runs, delegating its timer to the Web API.
3. The second setTimeout runs, also delegating its timer.
4. After approx. 1000ms, both timers expire and their callbacks go to the Task Queue.
5. The Event Loop moves the first callback onto the call stack and executes it.
6. Then, the Event Loop deals with the second callback.
7. Both callbacks execute nearly back-to-back, with only a minimal delay after the call stack’s free.
Even though both timers were set for 1000 milliseconds, they don’t execute exactly 1000ms apart. Instead, once the call stack is free, the Event Loop processes all queued tasks back-to-back. This illustrates that setTimeout guarantees a minimum delay before the callback is eligible to run, but not an exact delay between consecutive callbacks.
Now we also understand why setTimeout with a delay of 0 milliseconds might sound like it should run the callback immediately, but in practice it doesn’t. When you call setTimeout(fn, 0), the browser doesn’t execute fn right away. Instead, it hands off the callback to the Web API, which then places it in the Task Queue after the current execution context and any pending microtasks have been processed. This means that even with a 0-millisecond delay, the callback is executed only after the call stack is empty, and not until the next tick of the event loop. Essentially, the delay of 0 is a minimum wait time, ensuring that the function is deferred rather than being executed synchronously.
AAaaand - scene!
To wrap it up, understanding how the JavaScript event loop handles asynchronous tasks is key to building responsive and efficient apps. Once you see how the call stack, Web APIs, and task queue work together, it becomes clear that even though JavaScript runs on a single thread, it can still manage many operations without freezing up your UI. Whether you’re debugging UI slowdowns or using techniques like setTimeout to handle Angular’s change detection issues, these concepts give you the tools to write smoother, more maintainable code. With this knowledge, you’ll be in a better position to optimize your app’s performance and create a better experience for your users.
Commentaires