AI Guides

Deep Dive: JavaScript Event Loop, Microtasks, Macrotasks, and async/await

A developer-focused guide to JavaScript execution order across synchronous code, Promises, queueMicrotask(), setTimeout(), async/await, process.nextTick(), and setImmediate() in browsers and Node.js.

Published: Jun 5, 2026Updated: Jun 5, 2026Reading time: 7 minViews: 0
JavaScriptEvent LoopAsync AwaitPromisesNode.js

💡Key Takeaways

  • A developer-focused guide to JavaScript execution order across synchronous code, Promises, queueMicrotask(), setTimeout(), async/await, process.nextTick(), and setImmediate() in browsers and Node.js.

Updated: 05/06/2026
Topic: JavaScript Event Loop in browsers and Node.js
Goal: Help developers understand the real execution order of synchronous code, Promises, queueMicrotask(), setTimeout(), async/await, process.nextTick(), and setImmediate().

Quick summary

JavaScript is often described as running on a main thread, but that does not mean every line runs immediately in the visual order of the file. The event loop is the scheduling mechanism that decides when synchronous code, tasks, microtasks, timers, I/O callbacks, and rendering get a chance to run.

If you remember only one rule, remember this: after a task finishes, JavaScript drains the entire microtask queue before moving to the next task. This is why Promise.then() and the continuation after await usually run before setTimeout(..., 0).

JavaScript Event Loop Diagram
JavaScript Event Loop Diagram

Image source: PNG diagram created for this Markdown file based on MDN Web Docs and Node.js documentation. No SVG is used.

1. What problem does the event loop solve?

In a browser, JavaScript must coordinate script execution, clicks, timers, network responses, DOM work, layout, painting, and user interaction. If everything ran at once, behavior would be unpredictable. If everything blocked everything else, the UI would freeze.

The event loop is the scheduler that coordinates these operations. A simplified browser model is: run one task, then run pending microtasks, then the browser may render, then move to the next turn. MDN explains that a JavaScript agent has an execution context stack, a task queue, and a microtask queue; the event loop coordinates task execution, microtasks, and rendering in browser environments. [1][2]

In plain terms: the event loop is JavaScript’s runtime scheduler.

2. Three concepts you must separate

Call stack

The call stack is where JavaScript executes the current code. Synchronous code goes onto the call stack and runs first.

Example:

JS
console.log("A");
console.log("B");
console.log("C");

Output:

Example

A B C

Nothing from an async queue can interrupt these three lines because the current call stack has not finished.

Task, often called macrotask

A task is work placed into a task queue. In browsers, common examples include initial script execution, setTimeout() callbacks, setInterval() callbacks, click event callbacks, and some async API callbacks. MDN describes tasks as work scheduled by standard mechanisms such as running a program, dispatching an event, or firing a timeout/interval. [1]

Example:

JS
setTimeout(() => {
  console.log("timer");
}, 0);

Even with a delay of 0, the callback does not run immediately. It is queued as a task and must wait until current synchronous code finishes and the microtask queue has been drained.

Microtask

A microtask is a higher-priority queue than the task queue. Common examples include Promise handlers, queueMicrotask(), and the continuation after await once the awaited Promise settles. MDN emphasizes that once a task exits, the event loop runs microtasks until the microtask queue is empty; if a microtask enqueues more microtasks, those newly added microtasks also run before the next task. [1][2]

Example:

JS
Promise.resolve().then(() => {
  console.log("promise");
});

queueMicrotask(() => {
  console.log("microtask");
});

These callbacks do not run immediately in the current call stack. They are put into the microtask queue and run after the current synchronous code finishes.

3. The most important execution rule

In the browser, the simplified model is:

Example

1. Run synchronous code in the current task. 2. When the call stack is empty, drain the entire microtask queue. 3. The browser may render. 4. Take the next task. 5. Repeat.

Example:

JS
console.log("A");

setTimeout(() => {
  console.log("B - timeout");
}, 0);

Promise.resolve().then(() => {
  console.log("C - promise");
});

queueMicrotask(() => {
  console.log("D - queueMicrotask");
});

console.log("E");

Output:

Example

A E C - promise D - queueMicrotask B - timeout

Explanation:

A and E are synchronous, so they run first. setTimeout() queues a task. Promise.then() and queueMicrotask() queue microtasks. Once synchronous code finishes, the microtask queue is drained before the timer task runs.

4. async/await is Promise-based

An async function always returns a Promise. await makes code look synchronous, but internally it pauses the rest of the async function and resumes when the Promise is handled. MDN describes async function as returning a Promise, and await as an operator used to wait for a Promise and get its fulfillment value. [4][5]

Example:

JS
async function run() {
  console.log("1");
  await Promise.resolve();
  console.log("2");
}

console.log("0");
run();
console.log("3");

Output:

Example

0 1 3 2

Explanation:

console.log("0") runs first. Calling run() immediately runs the line before await, so 1 is printed. At await Promise.resolve(), the rest of the function is scheduled to continue later through Promise machinery. Therefore 3 prints before the async function resumes and prints 2.

Common misconception: await does not make JavaScript multithreaded. It only provides a clearer syntax for Promise-based asynchronous code.

5. Why setTimeout(..., 0) does not run immediately

setTimeout(..., 0) means “queue this timer callback as soon as possible after the minimum timer threshold,” not “run this right now.”

Example:

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

for (let i = 0; i < 3_000_000_000; i++) {
  // simulate heavy synchronous work
}

console.log("done");

The timeout callback cannot run while the loop occupies the call stack. JavaScript must wait until the stack is empty. If synchronous work is too heavy, UI rendering, timers, and user interactions can all be delayed.

6. Microtasks can starve tasks

Because the event loop drains microtasks until the queue is empty, repeatedly creating new microtasks can block the next task from running. MDN warns that microtasks can enqueue more microtasks and create a real risk of the event loop endlessly processing microtasks. [1]

Dangerous example:

JS
function loop() {
  queueMicrotask(loop);
}

loop();

setTimeout(() => {
  console.log("This line may never run");
}, 0);

This code keeps adding new microtasks before the event loop can move to the timer task. In production code, avoid using microtasks for long loops or CPU-heavy work.

7. When should you use microtasks?

You usually do not need queueMicrotask() in everyday application code. MDN notes that most developers will rarely need microtasks; they are more useful for framework/library internals or specific ordering problems. [1]

Use microtasks when:

  • You need cleanup immediately after the current code but before the next timer/event.
  • You need consistent callback order between a Promise branch and a non-Promise branch.
  • You are writing a library that needs to emit an event after internal state has stabilized.

Avoid microtasks when:

  • The work is CPU-heavy.
  • You need to let the browser render first.
  • You only need a simple delay; setTimeout() or another scheduling API is usually clearer.
  • You might recursively enqueue too many microtasks.

8. Important Node.js differences

Node.js has a more detailed event loop than browsers. The Node.js documentation describes phases including timers, pending callbacks, poll, check, and close callbacks. Node uses the event loop to support non-blocking I/O even though JavaScript runs on a single thread by default. [3]

Key Node.js points:

  • setTimeout() and setInterval() run in the timers phase.
  • I/O callbacks are commonly processed in the poll phase.
  • setImmediate() runs in the check phase.
  • process.nextTick() is not the same as a browser task or microtask; it has very high priority in Node.js and can delay other callbacks if abused.
  • Starting with libuv 1.45.0, corresponding to Node.js 20, timer behavior changed: timers run only after the poll phase, rather than both before and after it as in older versions. This can affect some interactions between setImmediate() and timers. [3]

Node.js example:

JS
console.log("start");

setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));

Promise.resolve().then(() => console.log("promise"));
process.nextTick(() => console.log("nextTick"));

console.log("end");

Typical output:

Example

start end nextTick promise timeout / immediate

The order between timeout and immediate can depend on the execution context and Node.js version, especially when code is inside or outside an I/O callback. Avoid writing logic that relies on setTimeout(..., 0) always beating setImmediate().

9. Common real-world bugs

Bug 1: Assuming a Promise handler runs immediately

Incorrect:

JS
let value = 0;

Promise.resolve().then(() => {
  value = 1;
});

console.log(value); // 0, not 1

Better:

JS
let value = 0;

await Promise.resolve().then(() => {
  value = 1;
});

console.log(value); // 1

If top-level await is not available, place the code inside an async function.

Bug 2: Assuming setTimeout(..., 0) is faster than a Promise

Incorrect mental model:

JS
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("promise"));

Typical output:

Example

promise timer

Promise handlers are microtasks; timer callbacks are tasks.

Bug 3: Putting heavy work in a microtask

Incorrect:

JS
queueMicrotask(() => {
  heavyCalculation();
});

If heavyCalculation() takes a long time, the UI is still blocked. Microtasks do not move work to another thread. In browsers, use a Web Worker for heavy work. In Node.js, consider worker threads, a job queue, or a separate service.

Bug 4: Using async/await without error handling

Incorrect:

JS
async function loadUser() {
  const res = await fetch("/api/user");
  return res.json();
}

loadUser(); // errors may become unhandled rejections

Better:

JS
async function main() {
  try {
    const user = await loadUser();
    console.log(user);
  } catch (error) {
    console.error("Load user failed:", error);
  }
}

main();

10. Debugging checklist for JavaScript execution order

When reading async JavaScript, ask these questions in order:

  1. Which lines are synchronous? They run first.
  2. Which lines create microtasks? Examples: Promise.then(), queueMicrotask(), continuation after await.
  3. Which lines create tasks? Examples: setTimeout(), event callbacks, timers.
  4. Does any microtask enqueue more microtasks?
  5. Is any synchronous code blocking the event loop?
  6. In Node.js, is there process.nextTick() or setImmediate()?
  7. Is the logic relying on an order that Node.js or the browser does not guarantee?

11. Conclusion

The event loop is not abstract theory. It explains why UI freezes, why Promises run before timers, why await does not make code parallel, and why a small recursive microtask can prevent other callbacks from running.

For modern JavaScript work—React, Vue, Node.js, API clients, queue workers, realtime apps—understanding the event loop makes debugging more accurate and prevents subtle asynchronous bugs.

References

[1] MDN Web Docs — Using microtasks in JavaScript with queueMicrotask()
https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide

[2] MDN Web Docs — In depth: Microtasks and the JavaScript runtime environment
https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth

[3] Node.js Docs — The Node.js Event Loop
https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick

[4] MDN Web Docs — async function
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

[5] MDN Web Docs — await
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await

[6] WHATWG HTML Standard — Event loop processing model
https://html.spec.whatwg.org/multipage/webappapis.html

PR

Written by PixelRouter Editorial Team

We publish deep, authoritative guides on AI infrastructure, API gateway security, cloud financial management, and system optimizations for developers.

FAQ

What is the most important rule of the JavaScript event loop?

After a task finishes and the call stack is empty, JavaScript drains the entire microtask queue before moving to the next task. This is why Promise handlers, queueMicrotask(), and continuations after await usually run before setTimeout(..., 0).

Why does setTimeout(..., 0) not run immediately?

setTimeout(..., 0) queues a timer callback as a task. It still has to wait for the current synchronous code to finish and for pending microtasks to be drained before it can run.

Does async/await make JavaScript multithreaded?

No. async/await is Promise-based syntax. An async function returns a Promise, and await pauses the rest of that function until the awaited Promise is handled, but it does not move work to another thread.

Can microtasks block other callbacks?

Yes. Because the event loop drains microtasks until the queue is empty, recursively adding microtasks can prevent the next task, such as a timer callback, from running.

How is Node.js event loop behavior different from browsers?

Node.js has phases such as timers, pending callbacks, poll, check, and close callbacks. setImmediate() runs in the check phase, process.nextTick() has very high priority, and the order between setTimeout(..., 0) and setImmediate() can depend on context and Node.js version.