stackademic

The leading education platform for anyone with an interest in software development.

The JavaScript Event Loop

How JavaScript schedules tasks, microtasks, and callbacks on a single thread

Overview

JavaScript runs on a single thread, yet it handles timers, network requests, and user events without blocking. The event loop makes this possible by pulling work from queues once the call stack is empty. Understanding the order of execution—synchronous code, then microtasks, then macrotasks—explains many surprising results.

Syntax / Usage

There is no special syntax for the event loop; it is a runtime mechanism. Promises schedule microtasks, while setTimeout and I/O schedule macrotasks. All queued microtasks drain before the next macrotask runs.

console.log('1: sync start')

setTimeout(() => console.log('2: timeout (macrotask)'), 0)

Promise.resolve().then(() => console.log('3: promise (microtask)'))

queueMicrotask(() => console.log('4: queueMicrotask'))

console.log('5: sync end')

// Output order:
// 1: sync start
// 5: sync end
// 3: promise (microtask)
// 4: queueMicrotask
// 2: timeout (macrotask)

Examples

Microtasks always finish before a timer, even a zero-delay one:

setTimeout(() => console.log('timeout'), 0)

Promise.resolve()
  .then(() => console.log('microtask A'))
  .then(() => console.log('microtask B'))

// microtask A
// microtask B
// timeout

async/await is built on the microtask queue—code after await resumes as a microtask:

async function run() {
  console.log('start')
  await null            // suspends, resumes on the microtask queue
  console.log('after await')
}

run()
console.log('sync after call')

// start
// sync after call
// after await

Long synchronous work blocks everything, including rendering and timers:

function blockFor(ms) {
  const end = Date.now() + ms
  while (Date.now() < end) {} // starves the event loop
}

setTimeout(() => console.log('I am late'), 10)
blockFor(100) // the timer cannot fire until this returns

Common Mistakes

  • Assuming setTimeout(fn, 0) runs immediately—it waits for the current stack and all microtasks
  • Blocking the thread with heavy loops, freezing UI and delaying every callback
  • Expecting promise callbacks to run before remaining synchronous code
  • Creating infinite microtask loops (a .then that always schedules another) that starve macrotasks
  • Confusing the order of nested timers and promises when debugging async bugs

See Also

promises async-await javascript-error-handling