How the JavaScript Event Loop and Task Queue Work: A Complete Guide
The JavaScript event loop enables single-threaded concurrency by continuously monitoring the call stack, executing all pending microtasks immediately after the stack clears, and then processing one macrotask at a time.
The JavaScript event loop is the orchestration mechanism that makes asynchronous programming possible in a single-threaded runtime. According to the comprehensive explanations in the lydiahallie/javascript-questions repository, specifically within the README.md file around lines 941-959, the event loop coordinates the call stack, microtask queue, and task queue to determine exactly when your code executes.
The Core Architecture of the JavaScript Event Loop
The Call Stack and Single-Threaded Execution
JavaScript operates on a single thread, meaning only one piece of code runs at any given moment. When a function is invoked, its execution context is pushed onto the call stack. The engine processes the code at the top of this stack until completion, then pops it off. As documented in the repository's explanation at line 957 of README.md, the event loop constantly looks at the stack to determine if it's safe to process queued tasks.
Web APIs and the Host Environment
Asynchronous operations like setTimeout, fetch, or DOM events are not part of the JavaScript engine itself—they belong to the host environment (browser or Node.js). When you invoke these APIs, the request is handed off to the host, and JavaScript immediately continues executing the current stack without waiting. The host environment manages these operations independently and prepares callbacks for insertion into the appropriate queues once complete.
Task Queues: Macrotasks vs. Microtasks
The Macrotask Queue (Task Queue)
Once an asynchronous operation completes, the host environment places its callback into the task queue (also called the macrotask queue). Typical macrotasks include setTimeout, setInterval, I/O callbacks, and UI rendering events. As detailed in the repository's visual walkthrough around lines 941-959 of README.md, the event loop pulls only one macrotask at a time for execution after clearing the microtask queue.
The Microtask Queue
Promises, MutationObserver, and explicitly queued microtasks via queueMicrotask utilize a separate microtask queue. These tasks have higher priority than macrotasks. According to the repository's explanation at lines 4359-4367 of README.md, after the call stack becomes empty, the event loop drains all pending microtasks before processing the next macrotask. This ensures that promise resolutions and async/await continuations execute as soon as possible.
Priority and Execution Order
The execution priority follows a strict hierarchy: Call Stack > Microtask Queue > Macrotask Queue. When the stack empties, the event loop first executes every microtask in FIFO order, then pulls a single macrotask. This cycle repeats indefinitely. The repository's detailed article reference at line 557 of README.md (linking to https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif) provides visual diagrams illustrating this exact flow.
Code Examples: Event Loop Behavior in Practice
Macrotasks vs. Microtasks
This example demonstrates the priority difference between setTimeout (macrotask) and Promises (microtask):
console.log('A'); // 1️⃣ Push onto stack, executes immediately
setTimeout(() => console.log('B'), 0); // 2️⃣ Handed to Web API, queued as macrotask
Promise.resolve().then(() => console.log('C')); // 3️⃣ Queued as microtask
console.log('D'); // 4️⃣ Runs before any queued callbacks
Output:
A
D
C // Microtask runs after stack empties
B // Macrotask runs after microtasks drain
As explained in the repository's walkthrough at lines 941-959 of README.md, the event loop processes C before B because microtasks have priority over macrotasks once the call stack clears.
Async/Await and Microtasks
The async/await syntax implicitly uses Promises, placing continuations in the microtask queue:
async function foo() {
console.log('1');
await Promise.resolve(); // Yields control, queues remainder as microtask
console.log('2');
}
foo();
console.log('3');
Output:
1
3
2
The await keyword pauses execution of foo and schedules the remaining code (console.log('2')) as a microtask. This executes after the current call stack completes (console.log('3')) but before any macrotasks, as detailed in the repository's microtask explanation at lines 4359-4367 of README.md.
Chained Microtasks
Multiple chained Promise handlers create sequential microtasks:
Promise.resolve()
.then(() => console.log('first'))
.then(() => console.log('second'));
console.log('sync');
Output:
sync
first
second
Each .then() appends a new microtask to the queue. The event loop drains the entire microtask queue before moving to macrotasks, ensuring first and second execute in order immediately after the synchronous code, as referenced in the repository at lines 4359-4367 of README.md.
Summary
- The JavaScript event loop enables asynchronous programming in a single-threaded environment by coordinating the call stack, microtask queue, and macrotask queue.
- Microtasks (Promises,
queueMicrotask,async/await) have higher priority than macrotasks (setTimeout, I/O, UI events) and execute immediately after the call stack empties. - The call stack processes synchronous code first, then the event loop drains all microtasks, then processes one macrotask, repeating this cycle indefinitely.
- According to the
lydiahallie/javascript-questionsrepository, specifically around lines 941-959 and 4359-4367 ofREADME.md, understanding this execution order is essential for predicting JavaScript runtime behavior.
Frequently Asked Questions
What is the difference between the microtask queue and the task queue in JavaScript?
The microtask queue handles higher-priority asynchronous operations like Promise resolutions and async/await continuations, while the task queue (or macrotask queue) manages lower-priority callbacks such as setTimeout, setInterval, and I/O operations. According to the lydiahallie/javascript-questions repository at lines 4359-4367 of README.md, the event loop always drains the entire microtask queue before processing a single macrotask.
Why does setTimeout with 0ms delay execute after Promise.then?
Even with a 0ms delay, setTimeout schedules its callback as a macrotask, while Promise.then places its handler in the microtask queue. As documented in the repository's visual walkthrough at lines 941-959 of README.md, the event loop prioritizes microtasks over macrotasks, executing all pending microtasks immediately after the call stack empties but before moving to the next macrotask.
How does async/await interact with the JavaScript event loop?
The async/await syntax is built on Promises, meaning every await expression effectively pauses function execution and schedules the remaining code as a microtask. When the awaited Promise resolves, the continuation is placed in the microtask queue and executes after the current call stack clears but before any macrotasks, as illustrated in the repository's examples at lines 4359-4367 of README.md.
Can microtasks starve the macrotask queue?
Yes, if microtasks are created recursively—for example, by chaining Promises or using queueMicrotask in a loop—they can prevent the event loop from reaching the macrotask queue indefinitely. According to the lydiahallie/javascript-questions repository, the event loop must drain the entire microtask queue before processing a single macrotask, meaning recursive microtask generation can effectively block timers and I/O callbacks from executing.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →