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
.thenthat 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