Chapter 25

The Event Loop: Spec-Level Definitions of Job Queue and Task Queue

JavaScript's single-threaded engine handles network requests, user clicks, and timers not through parallelism, but through a precise scheduling protocol called the Event Loop. Understand it and you understand the true nature of all JavaScript "asynchrony."

๐Ÿ”น Level 1 ยท What You Need to Know

The JavaScript engine is single-threaded: only one piece of code executes at any given moment. The illusion of concurrency comes from the Event Loopโ€”a continuous cycle that checks task queues, dequeues tasks, and executes them.

Two Task Categories: Macro-tasks and Micro-tasks

Dimension Macro-task (Task) Micro-task (Microtask)
Aliases Task Queue, Message Queue Job Queue, Microtask Queue
Sources setTimeout, setInterval, I/O callbacks, UI render events, postMessage Promise.then, queueMicrotask, MutationObserver (browser)
Execution timing One per event loop tick All flushed after the current task ends
Can produce new tasks Yes, appended to the queue Yes, new microtasks execute within the current flush
Spec source HTML spec ยง8.1.6 ECMAScript spec ยง9.5 (Jobs)

The most critical rule: After every macro-task completes, the engine flushes the entire microtask queue to emptyโ€”including microtasks generated by other microtasksโ€”before executing the next macro-task (or a render frame).

Priority Mnemonic

Sync code > Microtasks (all) > Render > Macro-task (one) > Microtasks (all) > Render > Macro-taskโ€ฆ

Basic Output Prediction Exercise

console.log('start');

setTimeout(() => console.log('timeout'), 0);

Promise.resolve().then(() => console.log('micro'));

console.log('end');

// Output: start โ†’ end โ†’ micro โ†’ timeout

Explanation:

  1. console.log('start') โ€” synchronous, runs immediately
  2. setTimeout โ€” registers a macro-task into the Task Queue
  3. Promise.resolve().then โ€” registers a microtask into the Microtask Queue
  4. console.log('end') โ€” synchronous, runs immediately
  5. Sync code finishes; flush microtask queue โ†’ micro
  6. Execute next macro-task โ†’ timeout

Common Misconceptions

Misconception 1: setTimeout(fn, 0) executes immediately

setTimeout(fn, 0) means "put this into the Task Queue no earlier than 0 ms from now," but it must wait until the current task and all microtasks finish. The actual minimum delay in browsers is typically 1โ€“4 ms (the HTML spec mandates a 4 ms throttle after 5+ nested levels).

Misconception 2: Microtasks and macro-tasks are both defined by JavaScript spec

The ECMAScript spec only defines "Jobs" (micro-tasks). The "Task Queue" and the overall event loop model are defined by the HTML spec (WHATWG). Node.js has its own implementation via libuv.

Misconception 3: There is only one microtask queue per loop

Browsers may maintain multiple macro-task queues (rendering, network, user input, etc.) and a scheduler decides which to pull from. However, there is exactly one microtask queue, and it must be fully drained after every macro-task.

Node.js Special Queues

Node.js adds two queues beyond standard ECMAScript microtasks:

Queue API Priority
nextTick queue process.nextTick Higher than Promise microtasks
Promise microtasks Promise.then Normal microtask
Timers phase setTimeout, setInterval Macro-task (libuv phase 1)
Check phase setImmediate Macro-task (libuv phase 5)

Node.js execution order: sync โ†’ nextTick โ†’ Promise microtasks โ†’ macro-tasks (by libuv phase).


๐Ÿ”ธ Level 2 ยท How It Actually Works

Complete Event Loop Execution Model

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Event Loop Tick                        โ”‚
โ”‚                                                          โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  Step 1: Dequeue one Task from Task Queue       โ”‚    โ”‚
โ”‚  โ”‚          (wait for new tasks if queue is empty) โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                      โ”‚                                   โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  Step 2: Execute that Task (run JS code)        โ”‚    โ”‚
โ”‚  โ”‚          Call stack: empty โ†’ active โ†’ empty     โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                      โ”‚                                   โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  Step 3: Drain Microtask Queue                  โ”‚    โ”‚
โ”‚  โ”‚    while (microtaskQueue.length > 0) {           โ”‚    โ”‚
โ”‚  โ”‚      task = microtaskQueue.shift()               โ”‚    โ”‚
โ”‚  โ”‚      task()   // may enqueue new microtasks      โ”‚    โ”‚
โ”‚  โ”‚    }                                             โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                      โ”‚                                   โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  Step 4: Rendering opportunity (browser only)   โ”‚    โ”‚
โ”‚  โ”‚    if (renderNeeded && frameTimeReached) {       โ”‚    โ”‚
โ”‚  โ”‚      render();                                   โ”‚    โ”‚
โ”‚  โ”‚    }                                             โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                      โ”‚                                   โ”‚
โ”‚                      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Back to Step 1 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The ECMAScript Job Mechanism

ECMAScript Chapter 9 (Executable Code and Execution Contexts) defines the Job concept.

A Job Queue is a FIFO list identified by name. The spec defines two built-in queues:

PromiseJobs    โ€” callbacks from Promise .then/.catch/.finally
ScriptJobs     โ€” eval() evaluation (rarely encountered in practice)

The EnqueueJob abstract operation:

EnqueueJob(queueName, job, arguments):
  1. Assert queueName is a known queue name
  2. Create a PendingJob record:
     { [[Job]]: job, [[Arguments]]: arguments,
       [[Realm]]: currentRealm, [[ScriptOrModule]]: currentScript }
  3. Append that record to the end of the named queue
  4. Return unused

When Promise.resolve().then(callback) executes, it internally calls EnqueueJob("PromiseJobs", PromiseReactionJob, [reaction, value]).

Tracking Call Stack, Task Queue, and Microtask Queue

setTimeout(() => console.log('T1'), 0);
Promise.resolve()
  .then(() => {
    console.log('M1');
    Promise.resolve().then(() => console.log('M2'));
  });
console.log('sync');
Step Call Stack Microtask Queue Task Queue Output
Initial [main] [] [] โ€”
setTimeout runs [main, setTimeout] [] [T1_cb] โ€”
Promise.then registers [main] [M1_cb] [T1_cb] โ€”
console.log('sync') [main, log] [M1_cb] [T1_cb] sync
main ends, stack empty [] [M1_cb] [T1_cb] โ€”
Run M1_cb [M1_cb] [] [T1_cb] M1
Inner Promise.then registers [M1_cb] [M2_cb] [T1_cb] โ€”
M1_cb ends [] [M2_cb] [T1_cb] โ€”
Run M2_cb [M2_cb] [] [T1_cb] M2
Microtasks empty; run macro-task [T1_cb] [] [] T1

Output order: sync โ†’ M1 โ†’ M2 โ†’ T1

async/await Microtask Expansion

async function foo() {
  console.log('foo start');
  await Promise.resolve();
  console.log('foo after await');
}

console.log('before');
foo();
console.log('after');

Desugared equivalent:

function foo() {
  console.log('foo start');
  return Promise.resolve(undefined).then(() => {
    console.log('foo after await');
  });
}

Execution trace:

  1. before โ€” sync
  2. foo start โ€” sync (inside foo body)
  3. await suspends, registers microtask for the continuation
  4. after โ€” sync (foo is paused, control returns to caller)
  5. Sync code ends; flush microtasks โ†’ foo after await

Output: before โ†’ foo start โ†’ after โ†’ foo after await

Node.js libuv Event Loop Phases

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚              Node.js Event Loop                  โ”‚
โ”‚                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚  timers  โ”‚โ†’ โ”‚ pending  โ”‚โ†’ โ”‚    idle,     โ”‚   โ”‚
โ”‚  โ”‚setTimeoutโ”‚  โ”‚callbacks โ”‚  โ”‚   prepare    โ”‚   โ”‚
โ”‚  โ”‚setInterval  โ”‚          โ”‚  โ”‚  (internal)  โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚       โ†‘                                  โ†“       โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚  close   โ”‚โ† โ”‚  check   โ”‚โ† โ”‚     poll     โ”‚   โ”‚
โ”‚  โ”‚callbacks โ”‚  โ”‚setImmed. โ”‚  โ”‚  (I/O wait)  โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                                                  โ”‚
โ”‚  After each phase: nextTick โ†’ Promise microtasks โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Key point: Node.js drains both nextTick and Promise microtask queues after each phase, not just after each top-level macro-task.

Classic Interview Question โ€” Full Analysis

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');
setTimeout(function () { console.log('setTimeout'); }, 0);
async1();

new Promise(function (resolve) {
  console.log('promise1');
  resolve();
}).then(function () {
  console.log('promise2');
});

console.log('script end');
Step Action Output Microtask Queue
1 console.log('script start') script start []
2 setTimeout registers โ€” [], Task:[cb]
3 Enter async1, then async2 async1 start, async2 โ€”
4 await suspends async1 โ€” [async1_cont]
5 Promise constructor (sync) promise1 [async1_cont]
6 .then registers โ€” [async1_cont, p2_cb]
7 console.log('script end') script end โ€”
8 Flush: run async1_cont async1 end [p2_cb]
9 Flush: run p2_cb promise2 []
10 Run macro-task setTimeout โ€”

Final output: script start โ†’ async1 start โ†’ async2 โ†’ promise1 โ†’ script end โ†’ async1 end โ†’ promise2 โ†’ setTimeout


๐Ÿ”บ Level 3 ยท How the Spec Defines It

ECMAScript Spec: ยง9.5 Jobs and Host Operations

From ECMAScript 2024, ยง9.5.1:

9.5.1 EnqueueJob ( queueName, job, arguments )

The abstract operation EnqueueJob takes arguments queueName (a String), job (a Job Abstract Closure), and arguments (a List of ECMAScript language values) and returns unused.

  1. Assert: queueName is "PromiseJobs".
  2. Let callerContext be the running execution context.
  3. Let callerRealm be callerContext's Realm.
  4. Let callerScriptOrModule be callerContext's ScriptOrModule.
  5. Let pending be the PendingJob Record { [[Job]]: job, [[Arguments]]: arguments, [[Realm]]: callerRealm, [[ScriptOrModule]]: callerScriptOrModule }.
  6. Perform any implementation-defined processing of pending.
  7. Add pending to the end of the Job Queue named by queueName.
  8. Return unused.

Key insight: The ECMAScript spec defines only EnqueueJob and the PromiseJobs queue. The question of "when to dequeue and execute" is delegated to the host environment. The HTML spec and Node.js each implement that "when" part differently.

HTML Spec: Processing Model for Event Loop

From the WHATWG HTML spec ยง8.1.7.3 (simplified pseudocode):

Run these steps forever:
  1. Let taskQueue be one of the event loop's task queues,
     chosen in an implementation-defined manner.
  2. Let oldestTask be the first runnable task in taskQueue;
     remove it from taskQueue.
  3. Set the event loop's currently running task to oldestTask.
  4. Run oldestTask's steps.
  5. Set the event loop's currently running task to null.
  6. Perform a microtask checkpoint.
  7. ... (update the rendering)

Microtask Checkpoint (ยง8.1.7.4):

To perform a microtask checkpoint:
  1. If performing a microtask checkpoint is true, return.
  2. Set the performing a microtask checkpoint flag to true.
  3. While the event loop's microtask queue is not empty:
     a. Let oldestMicrotask be dequeued from the queue.
     b. Set the event loop's currently running task to oldestMicrotask.
     c. Run oldestMicrotask's steps.
     d. Set the currently running task back to null.
  4. ... (notify about rejected promises)
  5. Set the performing a microtask checkpoint flag to false.

Step 3 is a while loopโ€”this is precisely why new microtasks generated during a microtask checkpoint are also executed within the same checkpoint.

PromiseReactionJob: The Actual Microtask Execution Unit

When a Promise resolves, the spec runs:

PromiseReactionJob ( reaction, argument ):
  1. Let promiseCapability be reaction.[[Capability]].
  2. Let type be reaction.[[Type]].
  3. Let handler be reaction.[[Handler]].
  4. If handler is empty:
     a. If type is Fulfill, let handlerResult be NormalCompletion(argument).
     b. Else: let handlerResult be ThrowCompletion(argument).
  5. Else: let handlerResult be Completion(Call(handler, undefined, ยซ argument ยป)).
  6. If promiseCapability is undefined, return unused.
  7. If handlerResult is a normal completion:
     return ? Call(promiseCapability.[[Resolve]], undefined, ยซ handlerResult.[[Value]] ยป).
  8. Else:
     return ? Call(promiseCapability.[[Reject]], undefined, ยซ handlerResult.[[Value]] ยป).

This Job is pushed via EnqueueJob("PromiseJobs", PromiseReactionJob, [reaction, value]).


๐Ÿ’Ž Level 4 ยท Edge Cases and Traps

Trap 1: Microtask Starvation of Macro-tasks

// This code permanently freezes the page
function drainAllMicrotasks() {
  queueMicrotask(drainAllMicrotasks);  // microtask spawns microtask, forever
}
drainAllMicrotasks();

// This setTimeout callback will never execute
// Render frames will never fire
// The page is completely frozen
setTimeout(() => console.log('will never run'), 0);

Why: The event loop cannot execute the next macro-task until the microtask queue is empty. If microtasks continuously spawn new microtasks, macro-tasks never get a chance. This is a real production trap โ€” even if each microtask is fast, infinite quantity equals an infinite loop.

Trap 2: Promise.resolve vs. new Promise โ€” Microtask Count Differences

const p = Promise.resolve(42);
Promise.resolve(p).then(v => console.log('B', v));  // same Promise, 1 microtask

new Promise(resolve => {
  resolve(Promise.resolve(42));  // resolve a thenable โ†’ extra microtask
}).then(v => console.log('C', v));

new Promise(resolve => {
  resolve(42);  // resolve a plain value โ†’ 1 microtask
}).then(v => console.log('D', v));

Output order: B โ†’ D โ†’ C

C is last because resolve(thenable) triggers NewPromiseResolveThenableJob โ€” an extra microtask to unwrap the thenable โ€” before registering the .then callback.

Trap 3: await Microtask Count Changed Across Spec Versions

Before V8 v7.2 (Node.js < 12), await generated 2 microtasks; afterward it was optimized to 1. This changed the answers to execution-order questions.

async function test() {
  await 1;
  return 'done';
}

test().then(console.log);
Promise.resolve().then(() => console.log('plain micro'));

// Node 10 output: plain micro โ†’ done  (await cost 2 microtask levels)
// Node 12+ output: done โ†’ plain micro  (await costs 1 microtask level)

This was a real breaking change affecting test suites.

Trap 4: Node.js process.nextTick vs. Promise.then Ordering

Promise.resolve().then(() => console.log('promise 1'));
process.nextTick(() => console.log('nextTick 1'));
Promise.resolve().then(() => console.log('promise 2'));
process.nextTick(() => console.log('nextTick 2'));

// Output: nextTick 1 โ†’ nextTick 2 โ†’ promise 1 โ†’ promise 2

Node.js drains the entire nextTick queue before touching the Promise microtask queue. Recursive process.nextTick calls can starve Promise callbacks just as infinite microtasks starve macro-tasks.

Trap 5: React 18 Automatic Batching and Microtasks

React 18 introduced automatic batching: multiple setState calls within the same event handler or async block are merged into a single render.

// Before React 18: 2 renders
setTimeout(() => {
  setCount(c => c + 1);  // render 1
  setFlag(f => !f);      // render 2
}, 1000);

// React 18: 1 render (automatic batching)
setTimeout(() => {
  setCount(c => c + 1);  // collected
  setFlag(f => !f);      // collected
  // Batch processed after the macro-task callback ends
}, 1000);

React 18's Scheduler uses MessageChannel (a macro-task source) to schedule the actual render work, while state collection happens synchronously within the current macro-task. This explains why act() in tests must flush both microtasks and macro-tasks to ensure renders complete.

Summary

  1. Each event loop tick dequeues one macro-task, then flushes all microtasks (including ones produced by microtasks).
  2. The ECMAScript spec only defines PromiseJobs (microtasks); the overall event loop is defined by the HTML spec or the host runtime.
  3. async/await in modern specs (ES2020+, V8 v7.2+) costs 1 microtask; older versions cost 2.
  4. Node.js process.nextTick has higher priority than Promise.then; both are "microtasks" but in separate queues.
  5. Infinitely recursive microtasks block macro-tasks (including rendering) permanently โ€” a real production hazard.
Rate this chapter
4.7  / 5  (5 ratings)

๐Ÿ’ฌ Comments