Episode 1 — Fundamentals / 1.17 — Asynchronous Programming JavaScript
1.17.a — Introduction to Asynchrony
In one sentence: JavaScript is single-threaded and uses an event loop to run async tasks — delegating work to browser APIs, then processing results via a callback queue and microtask queue — so the UI never freezes while waiting for slow operations.
Navigation: ← 1.17 Overview · 1.17.b — Callbacks →
1. Synchronous vs asynchronous execution
Synchronous code runs line by line. Each statement must finish before the next one begins.
console.log("A");
console.log("B");
console.log("C");
// Output: A, B, C — always in order
Asynchronous code starts an operation but does not wait for it to finish. The rest of the program continues, and the result is handled later.
console.log("A");
setTimeout(() => console.log("B"), 1000);
console.log("C");
// Output: A, C, B — "B" appears after ~1 second
| Aspect | Synchronous | Asynchronous |
|---|---|---|
| Execution | Line by line, blocking | Starts now, finishes later |
| Waiting | Current line must complete before the next | Other code runs while waiting |
| Example | Math.sqrt(9) | fetch("https://api.example.com") |
| Risk | Blocks the thread if slow | Requires careful coordination of results |
2. Why JavaScript needs asynchrony
JavaScript in the browser runs on a single thread — the same thread that handles:
- Parsing and executing your script
- Rendering the page (layout, paint)
- Handling user events (clicks, typing)
If a network request took 3 seconds synchronously, the browser would freeze — no scrolling, no clicking, no animations — for those 3 seconds.
Synchronous world (bad):
[Start request] -------- 3 seconds of NOTHING -------- [Response] [Render]
Asynchronous world (good):
[Start request] [Render] [Handle clicks] [Animate] ... [Response arrives → process it]
Non-blocking I/O is the core principle: start the operation, continue other work, handle the result when it arrives.
3. The event loop explained
The event loop is the mechanism that coordinates execution in JavaScript. It has several key parts:
3.1 The call stack
The call stack is where JavaScript keeps track of what function is currently running. It follows LIFO (Last In, First Out).
function greet(name) {
return "Hello, " + name;
}
function processUser(name) {
const message = greet(name); // greet() pushed onto stack
console.log(message); // greet() popped, console.log pushed
}
processUser("Alice"); // processUser pushed onto stack
Call stack over time:
1. [processUser]
2. [processUser, greet]
3. [processUser] ← greet returned
4. [processUser, console.log]
5. [processUser] ← console.log returned
6. [] ← processUser returned
3.2 Web APIs (browser-provided)
When you call setTimeout, fetch, or addEventListener, JavaScript delegates the actual waiting to the browser's C++ layer (Web APIs). The call stack is free to continue.
3.3 The callback queue (macrotask queue)
When a Web API finishes (timer fires, network response arrives), it places the callback in the callback queue (also called the macrotask queue). Examples of macrotasks:
setTimeout/setIntervalcallbacks- I/O callbacks
- UI rendering tasks
3.4 The microtask queue
Microtasks have higher priority than macrotasks. After each macrotask (or after the call stack empties), the engine drains all microtasks before moving to the next macrotask. Examples of microtasks:
.then()/.catch()/.finally()handlers on PromisesqueueMicrotask()callbacksMutationObservercallbacks
3.5 The loop itself
┌──────────────────────────────────┐
│ EVENT LOOP │
│ │
│ 1. Is the call stack empty? │
│ ├── No → keep executing │
│ └── Yes → go to step 2 │
│ │
│ 2. Any microtasks queued? │
│ ├── Yes → run ALL of them │
│ │ (then back to 2) │
│ └── No → go to step 3 │
│ │
│ 3. Any macrotasks queued? │
│ ├── Yes → run the OLDEST one │
│ │ (then back to 1) │
│ └── No → wait, then back │
│ to step 1 │
└──────────────────────────────────┘
4. Event loop in action — step-by-step trace
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
console.log("4");
| Step | Action | Call Stack | Microtask Queue | Macrotask Queue |
|---|---|---|---|---|
| 1 | console.log("1") executes | [log] | — | — |
| 2 | setTimeout registers callback | [setTimeout] | — | — |
| 3 | Timer fires immediately (0ms), callback goes to macrotask queue | — | — | [cb: log("2")] |
| 4 | Promise.resolve().then(...) — .then callback goes to microtask queue | — | [cb: log("3")] | [cb: log("2")] |
| 5 | console.log("4") executes | [log] | [cb: log("3")] | [cb: log("2")] |
| 6 | Call stack empty → drain microtasks → log("3") | [log] | — | [cb: log("2")] |
| 7 | Microtasks empty → run next macrotask → log("2") | [log] | — | — |
Output: 1, 4, 3, 2
5. ASCII diagram of the event loop
┌─────────────────┐
│ Your Code │
│ (Call Stack) │
└───────┬─────────┘
│ delegates async work
▼
┌─────────────────┐ ┌──────────────────┐
│ Web APIs │ │ Microtask Queue │
│ (setTimeout, │ │ (Promise .then, │
│ fetch, DOM │───────▶│ queueMicrotask) │
│ events) │ │ ★ Higher priority│
└───────┬─────────┘ └────────┬─────────┘
│ │
▼ │
┌─────────────────┐ │
│ Macrotask Queue │ │
│ (setTimeout cb, │ │
│ setInterval cb)│ │
└───────┬─────────┘ │
│ │
▼ ▼
┌──────────────────────────────────────────┐
│ EVENT LOOP │
│ 1. Run all microtasks until empty │
│ 2. Pick ONE macrotask, run it │
│ 3. Repeat │
└──────────────────────────────────────────┘
6. Real-world analogy: the restaurant waiter model
Think of a restaurant with a single waiter (the JavaScript thread):
| Restaurant | JavaScript |
|---|---|
| Waiter takes your order | Call stack executes your code |
| Waiter passes order to the kitchen | Code delegates to Web API (fetch, setTimeout) |
| Waiter serves other tables while kitchen cooks | Event loop runs other code while waiting |
| Kitchen bell rings — order is ready | Callback/Promise is placed in the queue |
| Waiter picks up the dish and delivers it | Event loop picks up callback and executes it |
The waiter never stands idle waiting at the kitchen window. If they did, every other table would be ignored — just like a synchronous blocking call freezes the UI.
7. How async enables browser responsiveness
Without asynchrony, every operation would block the main thread:
// HYPOTHETICAL synchronous fetch (this does NOT exist in browsers)
const data = syncFetch("https://api.example.com/data"); // 2 second wait
// During those 2 seconds: no clicks, no scrolling, no animations
// REAL asynchronous fetch
fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => renderUI(data));
// UI remains interactive while waiting for the response
Key insight: The browser's rendering engine shares the main thread with JavaScript. If JS blocks, the page becomes unresponsive. The event loop ensures that between each task, the browser gets a chance to repaint and handle user input.
8. Common async operations in JavaScript
| Operation | API | Type |
|---|---|---|
| Delayed execution | setTimeout / setInterval | Timer (macrotask) |
| Network requests | fetch / XMLHttpRequest | I/O |
| Reading files (Node.js) | fs.readFile | I/O |
| User events | addEventListener | Event-driven |
| Animations | requestAnimationFrame | Rendering |
| Database queries | Various drivers | I/O |
Key takeaways
- Synchronous = blocking, line by line. Asynchronous = non-blocking, result handled later.
- JavaScript is single-threaded — the event loop is how it handles concurrency without parallelism.
- The call stack executes code; Web APIs handle the waiting; queues hold ready callbacks.
- Microtasks (Promises) always run before macrotasks (
setTimeout) when the call stack is empty. - Async programming keeps the browser responsive — the UI thread is never blocked by waiting.
Explain-It Challenge
Explain without notes:
- Why does
setTimeout(() => console.log("X"), 0)not print"X"immediately? - If a Promise
.then()callback and asetTimeoutcallback are both queued at the same time, which runs first and why? - Describe the restaurant waiter analogy for how JavaScript handles async operations.
Navigation: ← 1.17 Overview · 1.17.b — Callbacks →