Skip to main content

JavaScript Asynchronous

JavaScript Event Loop

Written by Published

The event loop decides what runs next when the call stack is empty.

This is the mechanism behind everything in this section.

It also explains one of the most common interview questions in JavaScript.

Example

Example

javascript

console.log("first");

setTimeout(function () {
  console.log("fourth");
}, 0);

Promise.resolve().then(function () {
  console.log("third");
});

console.log("second");

The output is first, second, third, fourth.

The promise runs before the timer, even though the timer was scheduled first.

The Parts Involved

The call stack runs your code, one thing at a time.

The task queue holds timer and event callbacks.

The microtask queue holds promise callbacks, and always goes first.

Syntax

Syntax

javascript

// stack empties -> all microtasks -> one task -> repeat

Microtasks are drained completely before the next task.

Synchronous Code Goes First

Nothing queued can run until the current code has finished.

Example

Example

javascript

setTimeout(function () {
  console.log("last");
}, 0);

for (let i = 0; i < 3; i++) {
  console.log("loop " + i);
}

The whole loop finishes before the timer callback gets a turn.

Microtasks Beat Tasks

A promise callback is a microtask.

Every microtask runs before the next timer callback, whatever the delay.

Example

Example

javascript

setTimeout(function () {
  console.log("timer");
}, 0);

Promise.resolve().then(function () {
  console.log("promise");
});

The output is promise then timer.

Microtasks Can Queue More

A microtask that queues another still runs before any task.

The queue is drained completely, not one at a time.

Example

Example

javascript

setTimeout(function () {
  console.log("timer");
}, 0);

Promise.resolve()
  .then(function () {
    console.log("promise one");
    return Promise.resolve();
  })
  .then(function () {
    console.log("promise two");
  });

Both promise steps run before the timer.

Why a Long Loop Freezes the Page

Nothing in either queue can run while the stack is busy.

That includes clicks, rendering and timers.

Example

Example

javascript

const started = Date.now();

setTimeout(function () {
  console.log("the timer was delayed by the loop");
}, 0);

while (Date.now() - started < 30) {
  // deliberately blocking
}

console.log("loop finished");

The timer could not fire until the blocking loop released the thread.

await Uses Microtasks Too

Code after an await is resumed as a microtask.

Example

Example

javascript

async function run() {
  await Promise.resolve();
  console.log("after await");
}

setTimeout(function () {
  console.log("timer");
}, 0);

run();

console.log("sync");

The output is sync, after await, timer.

Complete Example

Complete Example

html

<!DOCTYPE html>
<html>
<head>
  <title>JavaScript Event Loop</title>
</head>
<body>

  <h1>The Event Loop</h1>

  <p id="out"></p>

  <script>
    const order = [];

    setTimeout(function () {
      order.push("timer");
      document.getElementById("out").textContent = order.join(" → ");
    }, 0);

    Promise.resolve().then(function () {
      order.push("promise");
    });

    order.push("synchronous");
  </script>

</body>
</html>

Try It Yourself

Run the above example in the Try It Editor.

Try predicting first:

Write down the order you expect before running it, then check.

Important Points

  • The call stack runs code one thing at a time.
  • Queued work waits until the stack is empty.
  • Microtasks are promise callbacks and run first.
  • Tasks are timer and event callbacks.
  • All microtasks are drained before the next task.

Conclusion

The event loop is why asynchronous JavaScript behaves the way it does.

Microtasks before tasks explains almost every ordering puzzle you will meet.

It is also one of the most reliably asked interview questions.