How the browser enables non-blocking, asynchronous JavaScript
Ibrahim GUOUAL
the problem
JavaScript is single-threaded
one Call Stack · one task at a time
# a slow task blocks everything else
Imagine a 30-second network request on the Call Stack. The entire page freezes. No clicks. No scrolling.
fetch("https://api.example.com/data")
// Would this block the Call Stack until data returns?
// That would freeze the entire UI…
Luckily, no. That's what Web APIs are for.
concept 1
The Call Stack execution context
function greet(name) {
return `Hello, ${name}!`
}
function respond() {
return greet("Lydia")
}
respond()
Each function call pushes a new execution context.
When it returns, it pops off the stack.
Call Stack (LIFO)
1respond() pushed → calls greet()
2greet("Lydia") pushed → returns string
3greet popped off
4respond popped off
5 Stack is empty ✓
concept 2
Web APIs offload work to the browser
Web APIs are a bridge between the JS runtime and browser features. They allow us to hand off long-running tasks, and free up the Call Stack immediately.
JS Runtime
Call Stack
Heap (memory)
Web APIs
fetch / XHR setTimeout Geolocation DOM events …and more
Browser
Network stack Rendering engine Sensors / Camera Timers …
Invoking a Web API is just registering a task with the browser, the execution context pops off the Call Stack right away.
// setTimeout, also callback-based
setTimeout(() => {
console.log("Timer done!")
}, 1000)
// delay = time until pushed to Task Queue
// NOT guaranteed execution time
What happens
1Function is pushed to Call Stack
2Browser takes over the async task
3Call Stack is freed immediately
4When done → callback → Task Queue
⚠️ A setTimeout(fn, 0) still goes through the Task Queue, the callback runs after any currently running code.
concept 4
The Event Loop the conductor
while (true) { if (callStack.isEmpty()) moveFromQueue() }
# continuously checks: is the call stack empty?
→ Call Stack is empty? Event Loop picks the first task from the Task Queue.
① Microtask QueuePromise handlers (.then, .catch, .finally), async/await continuations, MutationObserver, queueMicrotask → ALL microtasks are drained before moving on
② Task QueueCallback-based Web APIs: setTimeout, setInterval, event listeners, requestAnimationFrame → Processed one task at a time
⚠️ Microtasks can schedule more microtasks → infinite microtask loop is possible. This cannot happen with the Task Queue.