What is the event loop
The Event Loop
Javascript runs in a single-threaded and isolated process. It constantly sends requests to, and receives tasks from its runtime. The mechanism that determines in which order these tasks are executed is called the event loop.
TL;DR
The event loop runs when the stack is empty. It executes all queued microtasks first. Once the microtask queue is empty, it executes the next macrotask.
Call Stack
The call stack is the list of JavaScript instructions to execute. As the JS engine reads the script, it pushes instructions onto the stack, and removes them when they complete.
Although the call stack is single-threaded, JavaScript can perform concurrent operations by delegating work (such as timers or network requests) to its runtime.
Runtime
A runtime is an environment that sits between the operating system and the code. In this article, runtime refers to a JavaScript runtime. The most common examples are web browsers and NodeJS. Many other applications support JavaScript for scripting or extensions, but this article focuses on web browsers.
Browser API
Browser APIs are JavaScript's interface to the runtime. They gives access to the DOM, timers, files I/O, and networking. Those APIs are invoked through JavaScript functions' call.
Examples include setTimeout(func, 1000), elem.addEventListener("click", handler), and fetch(url). They all involve Browser APIs. Once the runtime completes an operation, it places the corresponding callback or promise into a task queue.
Task queues
When the runtime finishes its task (e.g. a timer expires), it pushes a new task either on the microtask queue for higher priority, or on the macrotask queue for lower priority.
Microtasks include promise reactions (async, await, then, catch, finally) and MutationObserver callbacks.
Macrotasks include timers, user interactions, and other events.
flowchart LR
A["Callstack"]
B["Microtask and <br> macrotask queues"]
subgraph "Browser APIs"
C["Timers"]
D["User events"]
E["HTTP"]
F["..."]
end
A --> C
A --> D
A --> E
A --> F
C --> B
D --> B
E --> B
F --> B
B --> A
Event loop
The event loop runs when the call stack is empty. If there are pending microtasks, it executes them one by one by pushing them onto the stack. When no microtasks remains, it executes the next macrotask.
If executing a task schedules a new microtask, those microtask are processed before any subsequent macrotask.
In browsers, additional work may occur between task executions. Typically, the browser runs all microtasks, then performs rendering and UI updates, and only afterward executes a single macrotask.
flowchart TB
idle(["Idle"])
stack_empty{"Stack <br> empty?"}
micro_wait{"Pending <br> Microtasks?"}
macro_wait{"Pending <br> Macrotasks?"}
microtask["Push next microtask <br> onto stack"]
macrotask["Push next macrotask <br> onto stack"]
idle --> stack_empty
stack_empty -- yes --> micro_wait
stack_empty -- no --> idle
micro_wait -- yes --> microtask
micro_wait -- no --> macro_wait
macro_wait -- yes --> macrotask
macro_wait -- no --> idle
microtask --> idle
macrotask --> idle
Notes
Some runtimes contain multiple micro and macrotask queues. While all microtasks have priority over macrotasks, tasks within each category may have different priority levels.
Additionally, although microtasks generally take precedence, runtimes may occasionally allow macrotasks to run to prevent starvation when microtasks are continuously queued.