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
AspectSynchronousAsynchronous
ExecutionLine by line, blockingStarts now, finishes later
WaitingCurrent line must complete before the nextOther code runs while waiting
ExampleMath.sqrt(9)fetch("https://api.example.com")
RiskBlocks the thread if slowRequires 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 / setInterval callbacks
  • 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 Promises
  • queueMicrotask() callbacks
  • MutationObserver callbacks

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");
StepActionCall StackMicrotask QueueMacrotask Queue
1console.log("1") executes[log]
2setTimeout registers callback[setTimeout]
3Timer fires immediately (0ms), callback goes to macrotask queue[cb: log("2")]
4Promise.resolve().then(...).then callback goes to microtask queue[cb: log("3")][cb: log("2")]
5console.log("4") executes[log][cb: log("3")][cb: log("2")]
6Call stack empty → drain microtasks → log("3")[log][cb: log("2")]
7Microtasks 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):

RestaurantJavaScript
Waiter takes your orderCall stack executes your code
Waiter passes order to the kitchenCode delegates to Web API (fetch, setTimeout)
Waiter serves other tables while kitchen cooksEvent loop runs other code while waiting
Kitchen bell rings — order is readyCallback/Promise is placed in the queue
Waiter picks up the dish and delivers itEvent 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

OperationAPIType
Delayed executionsetTimeout / setIntervalTimer (macrotask)
Network requestsfetch / XMLHttpRequestI/O
Reading files (Node.js)fs.readFileI/O
User eventsaddEventListenerEvent-driven
AnimationsrequestAnimationFrameRendering
Database queriesVarious driversI/O

Key takeaways

  1. Synchronous = blocking, line by line. Asynchronous = non-blocking, result handled later.
  2. JavaScript is single-threaded — the event loop is how it handles concurrency without parallelism.
  3. The call stack executes code; Web APIs handle the waiting; queues hold ready callbacks.
  4. Microtasks (Promises) always run before macrotasks (setTimeout) when the call stack is empty.
  5. Async programming keeps the browser responsive — the UI thread is never blocked by waiting.

Explain-It Challenge

Explain without notes:

  1. Why does setTimeout(() => console.log("X"), 0) not print "X" immediately?
  2. If a Promise .then() callback and a setTimeout callback are both queued at the same time, which runs first and why?
  3. Describe the restaurant waiter analogy for how JavaScript handles async operations.

Navigation: ← 1.17 Overview · 1.17.b — Callbacks →