# JavaScript Event Loop Phases: Macrotasks vs Microtasks

Source: https://devdocs10.com/articles/javascript/js-event-loop-phases  
By Murat Bo · Published 2026-07-28 · DevDocs10 (https://devdocs10.com)

JavaScript is single-threaded for your code, but it still handles timers, promises, DOM events, and network callbacks in a very specific order. This article explains how the event loop phases work, how macrotasks and microtasks are scheduled, and why some callbacks run sooner than others.

> **Quick answer:** Macrotasks are larger units of work like `setTimeout` callbacks and UI events, while microtasks are higher-priority jobs like promise reactions and `queueMicrotask` callbacks. After the current call stack finishes, JavaScript drains all microtasks before moving to the next macrotask.

> **Difficulty:** Beginner
>
> **You'll understand this better if you know:** basic functions, the idea of synchronous vs asynchronous code, and how promises or timers work at a simple level.

## 1. What Is the Event Loop?

The event loop is the scheduling system that lets JavaScript handle asynchronous work without running your code in parallel. It continuously checks whether the current call stack is empty, then pulls the next job from the appropriate queue and executes it.

- It coordinates synchronous code, timers, promise callbacks, and browser events.
- It helps JavaScript stay responsive even when many callbacks are waiting.
- It does not make JavaScript multi-threaded; it only controls when callbacks run.

When people say “the event loop,” they usually mean the overall mechanism that decides what runs next. The important practical split is between macrotasks and microtasks.

## 2. Why Macrotasks and Microtasks Matter

Understanding task order helps you predict logs, avoid race conditions, and write code that updates state at the right time. Many confusing bugs come from assuming that a timer, a promise, or a DOM callback will run immediately after the current line.

For example, a promise callback usually runs before `setTimeout(..., 0)`, even though both are asynchronous. That difference matters when you depend on execution order.

- It explains why promise handlers often run before timers.
- It helps you avoid unexpected UI updates and stale state.
- It makes debugging async code much easier.

## 3. Basic Syntax

At a high level, JavaScript processes work in this order:

1. Run the current synchronous code on the call stack.
2. Drain the microtask queue completely.
3. Pick the next macrotask and run it.
4. After that macrotask finishes, drain microtasks again.

Here is the smallest example that shows the difference:

```javascript
console.log("start");
setTimeout(() => {
  console.log("timeout");
}, 0);
Promise.resolve().then(() => {
  console.log("promise");
});
console.log("end");
```

Output (tested with Node 24.21.0):

```text
start
end
promise
timeout
```

This prints `start`, `end`, `promise`, and then `timeout`. The timer callback is a macrotask, while the promise handler is a microtask.

## 4. Step-by-Step Examples

### Example 1: Synchronous code always runs first

Before any queue is considered, JavaScript finishes the current script execution. That means plain function calls happen before promise handlers or timers.

```javascript
console.log("A");
console.log("B");
Promise.resolve().then(() => console.log("C"));
console.log("D");
```

Output (tested with Node 24.21.0):

```text
A
B
D
C
```

This prints `A`, `B`, `D`, then `C`. The promise callback waits until the synchronous script completes.

### Example 2: Microtasks run before timers

This example shows the most common ordering surprise. Even with a zero-delay timer, the promise callback still runs first.

```javascript
setTimeout(() => {
  console.log("timer");
}, 0);
Promise.resolve().then(() => {
  console.log("microtask");
});
```

Output (tested with Node 24.21.0):

```text
microtask
timer
```

The output is `microtask` and then `timer`. Timers are macrotasks and must wait until the microtask queue is empty.

### Example 3: queueMicrotask behaves like a promise reaction

`queueMicrotask` lets you schedule a microtask directly. It is useful when you want a callback to run after the current stack but before timers or rendering work that happens later.

```javascript
console.log("1");
queueMicrotask(() => {
  console.log("2");
});
console.log("3");
```

Output (tested with Node 24.21.0):

```text
1
3
2
```

This prints `1`, `3`, then `2`. A microtask is still deferred, but it runs before the next macrotask.

### Example 4: Nested scheduling changes the order

When a callback schedules more work, that new work is added to the correct queue. This can create layered ordering that looks odd until you remember the queue rules.

```javascript
setTimeout(() => {
  console.log("timeout 1");
  Promise.resolve().then(() => {
    console.log("microtask inside timeout");
  });
}, 0);
setTimeout(() => {
  console.log("timeout 2");
}, 0);
```

Output (tested with Node 24.21.0):

```text
timeout 1
microtask inside timeout
timeout 2
```

After the first timer finishes, its promise callback is placed in the microtask queue and runs before the second timer. That is why `microtask inside timeout` appears between the two timer logs.

## 5. Practical Use Cases

- Updating state after a synchronous function finishes, but before the browser handles the next task.
- Making promise chains predictable when several async steps depend on each other.
- Scheduling lightweight follow-up work with `queueMicrotask` instead of waiting for a timer.
- Separating user-input handling from deferred work so the UI stays responsive.
- Debugging why a callback fired earlier or later than expected.

## 6. Common Mistakes

### Mistake 1: Assuming setTimeout(0) runs immediately

A zero-delay timer does not mean “run next line.” It means “run in a future macrotask when the event loop gets there.”

**Problem:** The timer callback is scheduled after the current task and after any pending microtasks, so it will not run before promise handlers.

```javascript
console.log("before");
setTimeout(() => console.log("timer"), 0);
console.log("after");
```

Output (tested with Node 24.21.0):

```text
before
after
timer
```

**Fix:** If you need work to happen after the current stack but before the next macrotask, use a microtask.

```javascript
console.log("before");
queueMicrotask(() => console.log("microtask"));
console.log("after");
```

Output (tested with Node 24.21.0):

```text
before
after
microtask
```

The corrected version works because microtasks are drained before the event loop moves to the next macrotask.

### Mistake 2: Creating an endless microtask chain

Microtasks are processed until the queue is empty. If each microtask schedules another microtask, the browser or runtime can stay busy and delay rendering or timers.

**Problem:** This code keeps adding microtasks and can starve the event loop, so other work does not get a chance to run.

```javascript
function spin() {
  queueMicrotask(spin);
}
spin();
```

**Fix:** Break long work into macrotasks or add a limit so the browser can breathe between chunks.

```javascript
let count = 0;
function workChunk() {
  if (count >= 100) return;
  count++;
  setTimeout(workChunk, 0);
}
workChunk();
```

The fixed version yields control between chunks, which keeps the event loop responsive.

### Mistake 3: Confusing promise callbacks with regular synchronous code

Promise reactions are asynchronous, even when the promise is already resolved. This trips up developers who expect the resolved value to be available immediately.

**Problem:** The variable is still `undefined` when it is logged, because the `then` callback has not run yet.

```javascript
let value;
Promise.resolve(42).then(result => {
  value = result;
});
console.log(value);
```

Output (tested with Node 24.21.0):

```text
undefined
```

**Fix:** Read the value inside the microtask chain, or use `async` and `await` so the timing is explicit.

```javascript
async function showValue() {
  const value = await Promise.resolve(42);
  console.log(value);
}
showValue();
```

Output (tested with Node 24.21.0):

```text
42
```

The corrected version works because the log happens after the microtask that resolves the promise has completed.

## 7. Best Practices

### Practice 1: Use microtasks for short follow-up work

If the follow-up work is tiny and must happen before the next macrotask, `queueMicrotask` is a good fit. It keeps the ordering explicit without pretending the work is synchronous.

```javascript
function updateCache(value) {
  queueMicrotask(() => {
    console.log("cache updated for", value);
  });
}
```

This keeps the main call readable and reserves the microtask for a quick follow-up action.

### Practice 2: Use macrotasks to yield during heavy work

For long loops or chunked processing, macrotasks are often better because they let the browser handle input and rendering between chunks.

```javascript
const items = [1, 2, 3, 4, 5];
let index = 0;
function processNext() {
  if (index >= items.length) return;
  console.log(items[index++]);
  setTimeout(processNext, 0);
}
processNext();
```

Output (tested with Node 24.21.0):

```text
1
2
3
4
5
```

This approach prevents one large job from blocking the event loop for too long.

### Practice 3: Keep queue-sensitive logic simple and predictable

When execution order matters, reduce the number of nested callbacks. Clearer scheduling is easier to debug than deeply interleaved microtasks and timers.

```javascript
function loadThenRender() {
  return Promise.resolve()
    .then(() => "data")
    .then(data => {
      console.log(data);
    });
}
```

Simple chains are easier to reason about than mixing unrelated timers and promise handlers in the same flow.

## 8. Limitations and Edge Cases

- Microtasks can starve rendering if they keep rescheduling themselves.
- Timers are not guaranteed to run exactly at the requested delay; `0` means “as soon as possible,” not “immediately.”
- In browsers, DOM events and rendering are tied to the event loop, but rendering does not happen in the middle of your synchronous code.
- Different environments expose slightly different task sources. Browser event loops and Node.js event loops both have microtasks, but their internal phases are not identical.
- Promise callbacks and `queueMicrotask` callbacks share the same general microtask priority, so both run before the next macrotask.

## 9. Practical Mini Project

Let’s build a tiny logger that records the order in which different kinds of work run. This is useful for learning and for debugging async ordering in real projects.

```javascript
const log = [];
function record(label) {
  log.push(label);
}
record("sync 1");
queueMicrotask(() => record("microtask"));
setTimeout(() => record("macrotask"), 0);
record("sync 2");
setTimeout(() => {
  console.log(log);
}, 10);
```

Output (tested with Node 24.21.0):

```text
[ "sync 1", "sync 2", "microtask", "macrotask" ]
```

This example collects the order into an array and prints it later. A typical result is `["sync 1", "sync 2", "microtask", "macrotask"]`. It shows that synchronous work finishes first, then the microtask, then the timer callback.

## 10. Key Points

- The event loop decides when asynchronous callbacks run.
- Synchronous code always finishes before queued tasks are processed.
- Microtasks run before the next macrotask.
- Promise reactions and `queueMicrotask` callbacks are microtasks.
- `setTimeout` and similar callbacks are macrotasks.
- Too many microtasks can delay timers and rendering.

## 11. Practice Exercise

- Predict the output of a script that uses one synchronous log, one promise callback, one `queueMicrotask`, and one `setTimeout`.
- Then run it in your browser console or Node.js and compare the result.
- Change the order of the statements and see which parts stay the same.

**Expected output:** The synchronous logs appear first, then the microtasks in the order they were scheduled, and finally the timer.

**Hint:** Remember that all microtasks drain before the event loop moves to the next macrotask.

```javascript
console.log("sync");
Promise.resolve().then(() => console.log("promise"));
queueMicrotask(() => console.log("microtask"));
setTimeout(() => console.log("timeout"), 0);
```

Output (tested with Node 24.21.0):

```text
sync
promise
microtask
timeout
```

## 12. Final Summary

The event loop is the system that decides when JavaScript runs queued work. The key idea is simple: synchronous code runs first, then the microtask queue is drained, and only after that does the next macrotask run. This is why promise handlers and `queueMicrotask` callbacks usually happen before timers.

Once you internalize the difference between macrotasks and microtasks, async ordering becomes much easier to reason about. You will be better at debugging unexpected output, designing responsive UI code, and choosing the right scheduling tool for each job.

If you want to go further, next study the browser render cycle and how promises, timers, and input events interact with it in real applications.
