Rethinking the Event Loop
Background
I have been working on JavaScript for several years, and the event loop — a fundamental but abstract concept — is something I thought I fully understood after watching Philip Roberts's excellent talk at JSConf EU 2014.
However, while reading You Don't Know JavaScript, I came across the Job queue — similar to the task queue, but jobs execute after each task. When I searched for more information, even more unfamiliar terms appeared: macrotask and microtask. Maybe I don't understand the event loop as well as I thought.
Rethinking

Diagram from Philip Roberts's talk
Philip's talk demonstrated how asynchronous callbacks (such as setTimeout and I/O) are scheduled into the callback queue (also called the task queue). However, one important piece was missing: microtasks.
According to WHATWG — Event Loops:
- The event loop coordinates events, user interaction, scripts, rendering, and networking
- There are two kinds of event loops: browsing contexts and workers
- An event loop has one or more task queues
- Each event loop has a microtask queue
- When the JavaScript execution context stack becomes empty, a microtask checkpoint is performed
According to WHATWG — Integration with the JavaScript Job Queue:
- The JavaScript specification defines Jobs and Job Queues to specify how
Promiseoperations execute with a clean execution context stack and in a deterministic order - When the spec says to call
EnqueueJob, it queues a microtask
According to ECMA-262 — Jobs and Job Queues:
- Execution of a Job can only begin when there is no running execution context and the execution context stack is empty
According to You Don't Know JavaScript — Event Loop:
- Each iteration of the loop is called a tick. On each tick, if an event is waiting in the queue, it is dequeued and executed
Practice
Consider this code snippet:
setTimeout(function() {
console.log('A');
}, 0);
new Promise(function executor(resolve) {
console.log('B');
resolve();
}).then(function() {
console.log('C');
});
console.log('D');
The output is B D C A. Here is how the engine processes this step by step.
Steps
JS stack: []
macrotask queue: []
microtask queue: []
output: []
|
| setTimeout(..., 0) → scheduled as macrotask
v
JS stack: []
macrotask queue: [console.log('A')]
microtask queue: []
output: []
|
| Promise executor runs immediately (per MDN spec)
| console.log('B') → pushed to JS stack
v
JS stack: [console.log('B')]
macrotask queue: [console.log('A')]
microtask queue: []
output: []
|
| console.log('B') executes
v
JS stack: []
macrotask queue: [console.log('A')]
microtask queue: []
output: [B]
|
| resolve() called → .then() callback scheduled as microtask
v
JS stack: []
macrotask queue: [console.log('A')]
microtask queue: [console.log('C')]
output: [B]
|
| console.log('D') → pushed to JS stack and executes
v
JS stack: []
macrotask queue: [console.log('A')]
microtask queue: [console.log('C')]
output: [B, D]
All synchronous code has finished and the JS stack is empty.
Which queue runs next — macro or micro?
The microtask queue runs first. There are two ways to reason about this:
- Treat the whole script as a macrotask. Microtasks are drained at the end of each macrotask. Since the script itself is the first macrotask, its microtasks run next.
- Alternatively: when the JS stack becomes empty for the first time, the engine checks the microtask queue before picking the next macrotask.
Either way, the result is the same:
|
| JS stack empty → drain microtask queue first
| console.log('C') → pushed to JS stack
v
JS stack: [console.log('C')]
macrotask queue: [console.log('A')]
microtask queue: []
output: [B, D]
|
| console.log('C') executes
v
JS stack: []
macrotask queue: [console.log('A')]
microtask queue: []
output: [B, D, C]
|
| microtask queue empty → pick next macrotask
| console.log('A') → pushed to JS stack
v
JS stack: [console.log('A')]
macrotask queue: []
microtask queue: []
output: [B, D, C]
|
| console.log('A') executes
v
JS stack: []
macrotask queue: []
microtask queue: []
output: [B, D, C, A]
Done.
Conclusion
- task queue === macrotask queue
- job queue ≈ microtask queue — per You Don't Know JS, they are equivalent; per ECMA-262, there are two job types (
ScriptJobsandPromiseJobs), but the key takeaway is thatPromise.thencallbacks are microtasks - When the JS stack is empty, the engine picks the oldest task from the macrotask queue, executes it, then drains all pending microtasks, then picks the next macrotask
- Macrotasks:
script(whole script),setTimeout,setInterval,setImmediate, I/O, UI rendering - Microtasks:
process.nextTick,Promise.then,Object.observe,MutationObserver
References
Standards
- WHATWG — Event Loops
- WHATWG — Integration with the JavaScript Job Queue
- ECMA-262 — Jobs and Job Queues