JavaScript Event Loop Handling of Microtasks and Macrotasks: Complete Guide

JavaScript processes asynchronous operations through an event loop that prioritizes microtasks (Promise callbacks) over macrotasks (timers, I/O), executing all queued microtasks immediately after the current script and before the next macrotask begins.

The single-threaded nature of JavaScript relies on the event loop to orchestrate concurrent operations without blocking the main thread. Understanding how the JavaScript event loop handles microtasks and macrotasks is essential for predicting execution order and debugging async code. This analysis examines concrete implementations from the datawhalechina/easy-vibe repository, specifically its Vue/VitePress documentation components, to demonstrate these concepts in real-world scenarios.

The Event Loop Architecture

JavaScript environments maintain two distinct queues to manage asynchronous work:

Queue type Contents Execution timing
Microtask queue Promise callbacks (.then, .catch, .finally), queueMicrotask, process.nextTick (Node.js) Immediately after current script, before next macrotask
Macrotask queue setTimeout, setInterval, I/O callbacks, UI rendering, requestAnimationFrame At the start of the next event loop tick

The event loop follows a strict four-step process:

  1. Pull the next macrotask from the task queue and execute it.
  2. Process all queued microtasks that accumulated during that macrotask's execution.
  3. Perform rendering updates (if applicable).
  4. Return to step 1 for the next iteration.

This architecture ensures that microtasks always "jump ahead" of scheduled macrotasks, even when those macrotasks have zero delay.

Repository Implementation: Easy-Vibe Examples

The datawhalechina/easy-vibe repository demonstrates event loop mechanics through practical Vue composables and demo components. These files illustrate how mixing macrotasks and microtasks affects real-world async patterns.

The delay Helper in composables.js

In docs/.vitepress/theme/components/appendix/auth-design/shared/composables.js, the delay function creates a Promise that resolves via setTimeout:

export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

When await delay(0) executes, it schedules a macrotask (the setTimeout), while the await itself yields control back to the event loop as a microtask. The Promise resolves in the next tick, but only after all other microtasks complete, demonstrating how macrotasks defer execution while microtasks maintain priority.

useAsyncState Hook

The same file implements useAsyncState, which wraps async operations and manages loading states:

const execute = async (fn) => {
  isLoading.value = true
  error.value = null
  try {
    const result = await fn()          // ← creates a microtask
    data.value = result
    return result
  } catch (err) {
    error.value = err
    throw err
  } finally {
    isLoading.value = false
  }
}

Each await statement generates microtasks for the .then/.catch branches, ensuring state updates occur before the next macrotask runs. This pattern guarantees that UI updates (triggered by reactive assignments) batch efficiently within the same tick.

AsyncComparisonDemo Component

The AsyncComparisonDemo.vue file in docs/.vitepress/theme/components/appendix/async-task-queues/ visualizes async framework comparisons using reactive refs and computed properties:

const selected = ref('Celery')
const currentFw = computed(() => frameworks.find(f => f.name === selected.value))

While primarily a UI demonstration, this component relies on the same microtask/macrotask ordering when handling user interactions and data fetching sequences.

Execution Order Examples

Understanding the practical output requires examining how the event loop sequences mixed operations.

Basic Priority Demonstration

// Synchronous code runs first
console.log('A');

// Schedule a macrotask
setTimeout(() => console.log('B'), 0);

// Schedule a microtask via Promise
Promise.resolve().then(() => console.log('C'));

// More synchronous code
console.log('D');

Output order:


A   // synchronous
D   // synchronous
C   // microtask (drained before next macrotask)
B   // macrotask (next tick)

The setTimeout callback, despite having zero delay, executes only after the microtask queue empties.

await delay(0) Pattern

Using the delay helper from the Easy-Vibe repository:

import { delay } from '@/components/appendix/auth-design/shared/composables.js'

async function demo() {
  console.log('Start')
  await delay(0)      // → microtask waits for macrotask (setTimeout)
  console.log('After await')
}
demo()

The console outputs Start, then After await appears only after the setTimeout macrotask resolves in the next event loop iteration.

Promise Chain vs. Timer

console.log('1')
Promise.resolve().then(() => console.log('2'))   // microtask
setTimeout(() => console.log('3'), 0)           // macrotask
console.log('4')

Expected console output: 1 → 4 → 2 → 3

Why Task Priority Matters

Understanding microtask and macrotask distinction delivers three critical benefits:

  • UI Responsiveness: Updating state in microtasks ensures DOM changes apply before the next render frame, preventing visual stuttering.
  • Performance Optimization: Batching many microtasks together avoids extra paint cycles that separate macrotasks would trigger.
  • Predictable Ordering: Knowing that Promises resolve before timers helps eliminate race conditions where setTimeout callbacks accidentally execute after async operations complete.

Summary

  • The JavaScript event loop executes all microtasks (Promises) before processing the next macrotask (timers, I/O).
  • Microtasks include Promise.then, queueMicrotask, and process.nextTick, while macrotasks include setTimeout, setInterval, and rendering events.
  • The delay helper in datawhalechina/easy-vibe demonstrates how await creates microtasks that resolve after setTimeout macrotasks complete.
  • State updates using useAsyncState leverage microtask priority to batch UI changes efficiently within single event loop iterations.
  • Code execution order follows: synchronous → microtasks → macrotasks, regardless of timer delay values.

Frequently Asked Questions

What is the difference between microtasks and macrotasks in JavaScript?

Microtasks are lightweight, immediate jobs like Promise callbacks that execute immediately after the current script finishes, before the browser renders or handles user input. Macrotasks are heavier operations including setTimeout, setInterval, and I/O events that execute in subsequent event loop ticks, always after the microtask queue drains.

Why does Promise.resolve() execute before setTimeout(0)?

The event loop specification mandates that the JavaScript engine empty the entire microtask queue after completing the current script and before selecting the next macrotask. Since Promises queue as microtasks and setTimeout queues as macrotasks, Promise handlers execute first regardless of the timer's zero-millisecond delay.

How does the await keyword affect the event loop?

The await keyword pauses async function execution and schedules the resumption of that function as a microtask. When the awaited Promise resolves, the engine queues the continuation of the async function to the microtask queue, allowing it to run before any pending macrotasks but after the current synchronous code completes.

Can microtasks ever block the main thread?

Yes, microtasks can block the main thread if they create recursive Promise resolutions or infinite loops of queueMicrotask calls. Because the event loop drains the entire microtask queue before yielding to rendering or macrotasks, excessive microtask generation prevents the browser from updating the UI or processing user input, effectively freezing the interface.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →