The Event Loop — One Thread, No Blocking
IntermediateJavaScript runs your code on a single thread. Asynchronous work is handed to the runtime and its callback is queued, so nothing runs concurrently with your code — it runs after it.
Overview
This is the model that has to replace your threading instincts. In Java, concurrency means threads and you protect shared state with locks. In JavaScript there is one thread running your code, so there are no data races and no locks — but there is also nothing else to run your code while it is busy, which means a slow synchronous loop freezes the entire page. Async work does not happen in parallel with your function; it happens after your function and everything else on the stack has finished. Getting this right explains setTimeout(fn, 0) firing late, why await does not block anything else, and why a heavy computation makes the UI unresponsive.
Stack, Queues, Loop
The call stack runs your code. When it empties, the loop takes the next callback. Microtasks — promises — always drain completely before the next macrotask.
console.log('1') // sync
setTimeout(() => console.log('2'), 0) // macrotask queue
Promise.resolve().then(() => console.log('3')) // microtask queue
console.log('4') // sync
// Output: 1, 4, 3, 2
//
// 1 and 4 run on the stack, in order.
// The stack empties.
// ALL microtasks drain: 3.
// Then one macrotask: 2.
// This is why setTimeout(fn, 0) is not "immediately":
// it is "after the current stack and all pending promises".Microtasks Starve Macrotasks
Microtasks drain fully before any macrotask runs. A promise chain that keeps adding microtasks blocks timers and rendering indefinitely.
// This never lets a timer run:
function loop() { Promise.resolve().then(loop) }
loop()
setTimeout(() => console.log('never'), 0)
// Ordering with both kinds:
setTimeout(() => console.log('timeout'))
Promise.resolve().then(() => console.log('promise'))
queueMicrotask(() => console.log('microtask'))
// promise, microtask, timeout
// Node adds process.nextTick, which jumps ahead of promises:
process.nextTick(() => console.log('tick')) // before promise callbacksBlocking the Thread
The practical consequence: any long synchronous operation freezes everything, including rendering and clicks.
// Freezes the browser for the whole loop — no paint, no clicks
function sumTo(n) {
let total = 0
for (let i = 0; i < n; i++) total += i
return total
}
sumTo(1e10)
// Async does NOT fix this — await yields, it does not parallelise:
async function stillFrozen() {
await null
sumTo(1e10) // still blocks: it is synchronous work
}
// Real fixes: a Web Worker (another thread), or chunk the work
// and yield between chunks so the loop can paint.
async function chunked(items) {
for (let i = 0; i < items.length; i++) {
process(items[i])
if (i % 500 === 0) await new Promise(r => setTimeout(r))
}
}Key Points to Remember
- 1One thread runs your code — no data races, no locks, but also nothing else runs while you are busy
- 2Microtasks (promises) drain completely before the next macrotask (timers, I/O callbacks)
- 3setTimeout(fn, 0) means "after the current stack and all pending microtasks", not "now"
- 4async/await yields control; it does not move work to another thread — CPU-bound code still blocks
- 5For genuinely parallel CPU work you need a Web Worker; otherwise chunk and yield so the page can paint
Interview Questions
Sign in to ask AriaWhat does this print: console.log(1); setTimeout(()=>console.log(2)); Promise.resolve().then(()=>console.log(3)); console.log(4)?
What is the difference between a microtask and a macrotask?
Does making a function async make it run in parallel? Why or why not?
Ask Aria about The Event Loop — One Thread, No Blocking
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.