The JavaScript event loop is a core part of the JS runtime that coordinates asynchronous operations. It allows JavaScript to perform non-blocking work despite its single-threaded nature. While it is not something we interact with directly in day-to-day code, understanding how JavaScript processes work under the hood is useful. In practice, applications regularly deal with tasks that take time, such as network requests, I/O, or timers. If the main thread had to wait for those tasks to finish, the app would freeze. That is why JavaScript provides asynchronous patterns such as promises and async/await. Those patterns still need a system that keeps track of what runs when. That system is the event loop.
What Exactly Is the Event Loop?
The JavaScript event loop is a mechanism that allows JavaScript to perform non-blocking operations. It monitors the order in which function calls, web APIs, and promises are processed. A task that takes too long blocks the main thread and can freeze the app. To avoid that, the JavaScript runtime offloads long-running work to Web APIs or background threads. The event loop then solves the scheduling problem by deciding when the result of that work can return to the main thread.
Building Blocks
1. The Call Stack. The call stack is a central part of the JavaScript engine that handles function calls. It follows the Last In, First Out (LIFO) principle. Every function call creates a new stack frame and pushes it to the top of the stack. When a function finishes, that frame is removed.
2. Web APIs. Web APIs are features provided by the browser environment, not JavaScript itself. Other environments such as Node.js or Deno provide similar capabilities through their own systems, such as libuv in Node.js. These APIs allow JavaScript to handle timers, DOM work, and network requests outside the main thread.
3. Macrotask Queue. The macrotask queue, often called the callback queue or task queue, stores tasks deferred by Web APIs. It acts as a waiting area for tasks that will run when the call stack is empty.
4. Microtask Queue. The microtask queue stores asynchronous callbacks that have higher priority than macrotasks. The most common examples are promises and MutationObserver callbacks.
5. The Event Loop. The event loop is a continuous process that checks whether the call stack is empty and moves tasks from the microtask and macrotask queues to the stack in the correct order.
The event loop does not execute code by itself. It only decides when queued work can move back to the execution stack.
How Does the Event Loop Know What to Prioritize?
Because the event loop is responsible for task order, it follows a clear set of rules. Priority depends on the type of queue a task comes from and its position inside that queue.
1. Synchronous code takes absolute priority. If the call stack is not empty, the event loop keeps executing synchronous work before moving on to anything asynchronous.
2. Microtasks have higher priority than macrotasks. Once the stack is empty, the event loop processes all tasks inside the microtask queue. Since the queue is FIFO, those tasks run in the order they were added.
3. Macrotasks follow. After all microtasks finish, tasks delegated to the browser or another environment are handled one at a time. After each macrotask, the event loop checks again for any new synchronous work or microtasks.
These rules become easier to understand with a small example.
function demonstrateEventLoop() {
console.log('Start of script.')
setTimeout(() => {
console.log('Macrotask executed.')
}, 0)
Promise.resolve().then(() => {
console.log('Microtask executed.')
})
console.log('End of script.')
}
demonstrateEventLoop()We can trace the execution step by step.
Step 1: Executing synchronous operations. The global script runs and invokes demonstrateEventLoop, so that function is pushed onto the call stack. The first line inside it is synchronous, so Start of script. is printed immediately.
Step 2: Scheduling a macrotask. The next line uses setTimeout, which is provided by the Web API. That schedules the callback to be placed in the macrotask queue after a minimum delay of 0ms.
It is easy to assume this means it runs immediately, but it does not. The callback still has to wait until the call stack and the microtask queue are empty.
Step 3: Scheduling a microtask. The promise line creates a resolved promise, and the .then() callback is placed in the microtask queue. Even though it appears after the timeout in the code, it has higher priority during execution.
Step 4: Finishing the remaining synchronous work. The final console.log is another synchronous operation, so End of script. is printed right away.
Step 5: Processing the microtask queue. At this point, the call stack is empty. The event loop checks the microtask queue, finds the promise callback, and executes it. That prints Microtask executed.
Step 6: Processing the macrotask queue. Only after the microtask queue is fully drained does the event loop return to the macrotask queue. It finds the timeout callback there and executes it, which prints Macrotask executed.
The final output is:
Start of script.
End of script.
Microtask executed.
Macrotask executed.Conclusion
The event loop is a core part of JavaScript that makes asynchronous programming possible. Understanding it helps you reason about execution order and avoid subtle bugs.
If you want to go deeper, these are good follow-up resources:
- Lydia Hallie's YouTube video on the JavaScript event loop
- MDN's execution model documentation
