# The event loop

> One thread, a task queue and a microtask queue — why a promise callback runs before a setTimeout.

Concept · The browser · https://levelcode.org/concepts/event-loop

Your JavaScript runs on **one thread**. It never runs two pieces of your code at once, which is why you don't need locks. Waiting work like timers, network requests and user input happens elsewhere in the browser. When it finishes, a callback is queued, and the **event loop** runs queued callbacks one at a time.

## The loop, simplified

```text
repeat:
  1. run one task (a script, a timer callback, an event handler) to completion
  2. run ALL microtasks (promise callbacks, queueMicrotask), including new ones they add
  3. if it's time, render (requestAnimationFrame callbacks, style, layout, paint)
```

- **Tasks** (often called macrotasks) come from `setTimeout`, `setInterval`, DOM events and `MessageChannel`.
- **Microtasks** come from `.then`/`await` continuations and `queueMicrotask`. The whole queue drains after **every** task, before the next task and before rendering.

```js
console.log('1 script')
setTimeout(() => console.log('4 timeout (task)'), 0)
Promise.resolve().then(() => console.log('3 promise (microtask)'))
console.log('2 script')
// 1 script, 2 script, 3 promise (microtask), 4 timeout (task)
```

`await` is a microtask too. Everything after an `await` runs later, after the current task finishes.

## What it explains

- **A long task freezes the page.** While your function runs, nothing else does: no clicks, no rendering. Anything over about 50 ms is a "long task," and long tasks are what hurt [INP](https://levelcode.org/concepts/core-web-vitals). Split the work into chunks with `await` on a timer or `scheduler.yield()`, or move it to a Web Worker.
- **`setTimeout(fn, 0)` isn't immediate.** It waits for the current task and all microtasks, and browsers clamp nested timers to at least 4 ms.
- **Microtasks can starve rendering.** A promise chain that keeps scheduling more microtasks never lets the browser paint.
- **DOM changes don't paint until the task ends.** Setting a style and then removing it in the same task never shows on screen. See [how the browser renders](https://levelcode.org/concepts/critical-rendering-path).

Node.js has the same model with a few extra phases. Server languages with real threads trade this simplicity for [data races](https://levelcode.org/concepts/data-races) you have to prevent yourself.
