---
title: 'Asynchronous JavaScript and the Runtime'
source: 'https://academia.sh/en/courses/asynchronous-javascript'
language: en
updated: '2026-08-17T04:41:26+00:00'
license: 'CC BY-SA 4.0'
lessons: 17
outcomes:
    - 'Explains the task and microtask queues of the event loop in order'
    - 'Builds promise-based flows together with their error handling'
    - 'Diagnoses memory leaks and performance problems with tools'
---

# Asynchronous JavaScript and the Runtime

The single-threaded execution model, the task and microtask queues of the event loop, the state machine of promises, cancellation and timeouts, the memory lifecycle, and performance diagnosis.

## Execution Model

The consequences of single-threaded execution; the event loop's call stack and task and microtask queues; what timers actually mean; and callback-based asynchrony.

### The Single-Threaded Model

Source: https://academia.sh/en/courses/asynchronous-javascript/single-threaded-model

JavaScript's single-threaded, run-to-completion execution model; the consequences of blocking work; the boundary between the language's core and the asynchronous abilities the runtime provides.

The previous two courses built the language's value model: values, objects, the
prototype chain, and closures. All of it was explained under a single assumption — code
runs in the order it is written, one statement ends, the next begins. This course lifts
that assumption.

When you ask a measurement station for data, the response does not arrive right away.
What does the program do while the response is awaited? The answer "it waits" costs a
great deal in a single-threaded language. This lesson establishes why the decision to
wait cannot be made, and what the language does instead.

#### What a Single Thread Means

A **thread** is a single flow of execution that runs commands in order. A JavaScript
program has only one of these flows: at any given moment, a single line of user code
runs. Two functions never advance at the same time, two loops never interleave, two
callbacks never collide.

This has a direct consequence: **there is no data race.** No other code can slip in
between two lines where you read and write a variable. A counter increment that would
need a lock in other languages needs none here.

The operating system's process and thread concepts, kernel-level scheduling, and real
parallelism are a separate subject, covered in the Operating System Concepts course.
This course focuses on the code that falls to a single thread.

#### Run-to-Completion

The model's second rule is as binding as the single thread: once a piece of work starts
running, it is not broken up until it finishes on its own. This is called
**run-to-completion**.

This means that even a piece of work registered as "run after zero milliseconds" waits
for the work ahead of it to finish.

```js
function computeSum(rounds) {
  let total = 0;
  for (let i = 0; i < rounds; i += 1) total += i % 7;
  return total;
}

setTimeout(() => console.log("3 — timer callback"), 0);

console.log("1 — computation starts");
const result = computeSum(5_000_000);
console.log("2 — computation done, result:", result);
```

```
1 — computation starts
2 — computation done, result: 14999995
3 — timer callback
```

The callback was registered first, with a delay of zero; yet it ran last.
`setTimeout` does not run a piece of work *now*, it *queues* it. Queued work is only
taken up once the currently running code finishes. This principle is opened up in
detail in the next lesson.

Run-to-completion is the natural companion of a single thread. If you assumed another
callback could run in the middle of a running function, you would be back to a
discussion of locks and reentrancy. The model chooses simplicity to keep this
complexity away from the person writing the program — its cost is that every piece of
work in the queue waits for the one ahead of it.

#### Blocking Work

**Blocking** work is work that keeps the thread busy in a way that lets it do nothing
else. It has two sources.

The first is a long-running computation: sorting an array with millions of elements,
parsing a large piece of text. These actually do the work, they just take a long time.

The second, and more insidious, is **synchronous I/O** calls. A call that reads a file
synchronously holds the thread until the disk responds; yet during this time the thread
does no computation at all, it just waits.

```js
import { writeFileSync, readFileSync, readFile } from "node:fs";

writeFileSync("measurements.txt", "A1;21.4\nB2;19.8\nC3;23.1\n");

readFile("measurements.txt", "utf8", (error, data) => {
  console.log("3 — asynchronous read done:", data.trim().split("\n").length, "lines");
});

const content = readFileSync("measurements.txt", "utf8");
console.log("1 — synchronous read done:", content.trim().split("\n").length, "lines");
console.log("2 — rest of the script");
```

```
1 — synchronous read done: 3 lines
2 — rest of the script
3 — asynchronous read done: 3 lines
```

Both reads read the same file and produce the same result. Where they part is who owns
the thread while waiting. The synchronous one holds it; the asynchronous one releases
it and reports the result with a callback once it is ready.

Blocking's visible consequence varies by environment. In a browser, the interface
freezes: clicks are not handled, drawing does not update, scrolling sticks. On a
server, a single request's long computation makes every request arriving in the
meantime wait in a queue. In both cases the cause is the same: the thread is one, and
the work in the queue is patient.

#### Where the Language Ends and the Runtime Begins

The language's core does not define asynchrony. The standard defines promises and
`async` functions; but none of the concepts like "after ten milliseconds" or "once the
file is read" appear anywhere in the language's syntax.

These abilities are supplied by the **host environment**. The distinction introduced
in the JavaScript Fundamentals course becomes functional here:

| Layer | What it supplies |
|---|---|
| Language core | Values, functions, promises, `async`/`await`, the microtask rule |
| Host environment | Timers, network requests, file access, events, the task queue, the event loop |

`setTimeout` is not a language keyword; it is a function the host environment makes
available. The function that starts a network request belongs to the host environment
in the same way. This is why the same JavaScript code meets different abilities in
different environments: a browser has a document object, a server runtime has a file
system.

The distinction also splits responsibility. Waiting is not done by the *language*. The
host environment sets up the timer and, once the duration elapses, places the callback
in the queue; once the thread is free, JavaScript runs that callback. While waiting, the
thread is free.

#### Asynchrony Is Not Parallelism

This distinction holds throughout the course and needs to be settled from the start.

**Parallelism** is two pieces of work running at the same time; it requires more than
one thread. **Asynchrony** is a piece of work being started and its result picked up
later; it is possible with a single thread too.

When you start three measurement requests "at the same time," the waiting time these
requests spend on the network really does overlap — because the waiting is done by the
host environment, not JavaScript. But the code that processes the responses runs in
order: while the first response is being processed, the second waits in the queue. So
your program is parallel in waiting, sequential in computation.

The practical consequence is this: asynchrony speeds up I/O-heavy work; it does not
speed up computation-heavy work. Putting a million-element sort inside `setTimeout`
does not speed it up, it only delays its start. To actually spread out a computational
load, a separate thread is needed; runtimes offer this as a separate execution context,
and by default only messages are passed between contexts — ordinary variables are not
shared. The one exception is explicitly established shared memory (`SharedArrayBuffer`);
there the data race returns and synchronization is required.

#### The Course's Example: A Measurement Stream

Throughout the course, a single concrete example will be worked through: a small
program collecting temperature values from a few measurement stations. The stations'
response delays differ; some return an error, some never respond at all.

This example will go through the following steps, lesson by lesson: a single
measurement with a callback, a measurement chained with a promise, a batch of
measurements with combinators, a measurement bounded by a timeout, a cancellable
measurement, and finally a continuous measurement stream that leaves no leak.

Nothing printed throughout the program will ever be a duration; only **order** will be
printed. Duration varies by machine and load; order is the model's rule.

#### Summary

- JavaScript user code runs on a single thread; two pieces of code never advance at the
  same time, so no data race occurs.
- Under the run-to-completion rule, the work in the queue is not taken up until the
  running work finishes; even a zero-delay callback waits for this.
- Blocking work keeps the thread busy and delays every waiting piece of work; a long
  computation and synchronous I/O are its two sources.
- Timers, network, and file access are abilities of the host environment, not the
  language; the language defines only promises and the microtask rule.
- Asynchrony overlaps waiting, it does not parallelize computation.

#### Next Step

This lesson said callbacks "get in queue" but did not say how the queue works. It was
not shown that not every piece of work in the queue is equal, or that a promise
callback runs before a timer callback registered earlier. The next lesson opens up this
mechanism: it establishes the event loop's full rule through the distinction between
the call stack, the task queue, and the microtask queue, and justifies a mixed
example's output order line by line.

### The Event Loop

Source: https://academia.sh/en/courses/asynchronous-javascript/event-loop

The distinction between the call stack, the task queue, and the microtask queue; the event loop's turn rule; justifying a mixed example's output order line by line.

The previous lesson said callbacks "get in queue." There is not one queue, and the work
inside it is not equal in priority. A promise callback runs before a timer callback
registered before it; this is not a matter of priority, it is a difference of queue.

This lesson establishes those queues and the rule between them. Once the rule is
learned, a mixed program's output order becomes something you can state before running
it — this is the base skill used throughout the rest of this course.

#### The Call Stack

The **call stack** is the structure holding the function calls currently running. The
call frame concept introduced in the Programming Fundamentals course applies here
exactly as it was: every call pushes a frame, every `return` pops one.

Once the stack is empty, there is no currently running work left. This is the exact
statement of the previous lesson's run-to-completion rule: **the next piece of work
only starts once the stack is empty.**

This has an observable consequence. A `try` block only catches errors that occur while
its own stack frame is alive. A callback, on the other hand, runs on an empty stack, in
an entirely different turn:

```js
process.on("uncaughtException", (error) => {
  console.log("2 — caught at the runtime level:", error.message);
});

try {
  setTimeout(() => {
    throw new Error("from inside the callback");
  }, 0);
  console.log("1 — try block finished without issue");
} catch (error) {
  console.log("this line never runs:", error.message);
}
```

```
1 — try block finished without issue
2 — caught at the runtime level: from inside the callback
```

The `try` block covers only the `setTimeout` call itself; it does not cover the
callback's body. By the time the callback runs, the `try` block's frame has long since
left the stack. This observation will explain, later in the course, why asynchronous
error handling needs a separate mechanism.

#### The Task Queue and the Event Loop

The host environment places completed work into the **task queue**: a timer whose
duration has elapsed, an incoming network response, a clicked button. Every entry in
the queue is a **task** — in some sources, macrotask — meaning a callback to be run
start to finish.

The **event loop** is the control flow that repeats two steps forever:

1. If the stack is empty, take one task from the task queue and run it to completion.
2. Once the task is done, fully drain the microtask queue.

These two steps are called a **turn**. In a browser environment, a render step also
falls between turns; this is why a long task also delays drawing.

#### The Microtask Queue

A **microtask** is a small piece of work guaranteed to run at the end of the same turn.
Promise callbacks and work registered with `queueMicrotask` enter this queue.

The difference between the two queues can be stated in one sentence: **one task is
taken from the task queue per turn; the microtask queue is drained completely.**

New microtasks added during the draining are included in that same draining. That is,
if a microtask produces another microtask, that one also runs before moving on to the
next task.

```js
setTimeout(() => {
  console.log("A — first task");
  queueMicrotask(() => console.log("B — first task's microtask"));
  setTimeout(() => console.log("D — task produced by the first task"), 0);
}, 0);

setTimeout(() => console.log("C — second task"), 0);
```

```
A — first task
B — first task's microtask
C — second task
D — task produced by the first task
```

The first task produced both a microtask and a task. The microtask ran before moving
to the second task; the produced task, since it was added to the end of the queue,
stayed for last.

#### Order in a Mixed Example

The rule is tested in an example where synchronous code and both queues appear
together. You should be able to state the following program's output order before
running it.

```js
console.log("1 — synchronous");
setTimeout(() => console.log("6 — task queue (setTimeout)"), 0);
Promise.resolve().then(() => console.log("4 — microtask (promise)"));
queueMicrotask(() => console.log("5 — microtask (queueMicrotask)"));
console.log("2 — synchronous");
(function () {
  console.log("3 — synchronous (inner function)");
})();
```

```
1 — synchronous
2 — synchronous
3 — synchronous (inner function)
4 — microtask (promise)
5 — microtask (queueMicrotask)
6 — task queue (setTimeout)
```

The reasoning, line by line:

- **1, 2, 3** — These are the running work itself. No queue is looked at until the
  stack is empty. The `setTimeout`, `then`, and `queueMicrotask` calls among these
  lines only *register*; they do not run the function they register.
- **4, 5** — The stack is empty. The microtask queue is drained completely. The two
  pieces of work inside it run in registration order: the promise callback first, then
  `queueMicrotask`.
- **6** — The microtask queue is empty. Only now is a task taken from the task queue.

The source of the order is not priority itself, it is queue structure: `setTimeout`,
even though registered first, stayed for last — because it went into a different
queue.

#### The Microtask Queue Can Starve the Other

The "drained completely" rule has a cost: a microtask chain that keeps re-registering
itself keeps the task queue waiting indefinitely.

```js
let count = 0;

setTimeout(() => console.log("timer ran; accumulated microtasks:", count), 0);

function chain() {
  count += 1;
  if (count < 100000) queueMicrotask(chain);
}
queueMicrotask(chain);
```

```
timer ran; accumulated microtasks: 100000
```

All one hundred thousand microtasks ran before the zero-delay timer. Had the counter's
limit been removed, the timer would never have run at all. This is called
**starvation**: one queue consumes the other's chance to run.

The practical consequence is this: splitting a long, divisible piece of work into
microtasks is not a fix. To actually give the host environment a turn, the work has to
be handed to the task queue. This pattern will be detailed in the performance topic.

#### Applying the Rule to the Measurement Stream

The rule works exactly the same way in the course's example. The program below requests
measurements from two stations; the stations' response delays differ, and once a
response arrives, a recording task is left to a microtask.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
};

function requestMeasurement(station, callback) {
  console.log("request registered:", station);
  const record = SOURCE[station];
  setTimeout(() => callback({ station, value: record.value }), record.delay);
}

requestMeasurement("A1", (measurement) => {
  console.log("task: processing response —", measurement.station);
  queueMicrotask(() => console.log("microtask: record complete —", measurement.station));
});

requestMeasurement("B2", (measurement) => {
  console.log("task: processing response —", measurement.station);
  queueMicrotask(() => console.log("microtask: record complete —", measurement.station));
});

console.log("script's synchronous part finished");
```

```
request registered: A1
request registered: B2
script's synchronous part finished
task: processing response — B2
microtask: record complete — B2
task: processing response — A1
microtask: record complete — A1
```

Two observations matter. First, request order and response order are not the same:
A1 was requested first, B2 responded first. In an asynchronous flow, the order results
arrive in does not depend on the order requests were started. Second, each response's
recording microtask runs at the end of its own turn, before the next response is
processed; the two stations' processing steps never get mixed up with each other.

#### Queue Details Are Environment-Dependent

The two-queue model is standard and holds in every environment. Runtimes, however, can
split the task queue into phases and add their own extra queues. Where these extra
queues sit relative to promises' microtask queue can vary by context, even within the
same runtime.

The two files below contain the same three lines; only their module form differs.
Module forms themselves are the subject of the Modules, Tooling and the Ecosystem
course.

```js
// order.cjs
Promise.resolve().then(() => console.log("promise microtask"));
process.nextTick(() => console.log("runtime-specific queue"));
console.log("synchronous");
```

```
$ node order.cjs
synchronous
runtime-specific queue
promise microtask
```

```js
// order.mjs
Promise.resolve().then(() => console.log("promise microtask"));
process.nextTick(() => console.log("runtime-specific queue"));
console.log("synchronous");
```

```
$ node order.mjs
synchronous
promise microtask
runtime-specific queue
```

The relative order of the two queues flipped. What should be drawn from this is not
which form is correct: **portable code should not rely on the relative order of
non-standard queues.** The three-phase order among synchronous code, microtasks, and
tasks is reliable in every environment; beyond that is the environment's own detail.

#### Summary

- The call stack holds the running calls; the next piece of work only starts once the
  stack is empty.
- On every turn, the event loop takes one task from the task queue, then fully drains
  the microtask queue.
- Microtasks run in the same turn, tasks in later turns; this is why a promise callback
  registered later runs before a timer registered earlier.
- Microtasks produced during the draining are included in that same draining; this can
  starve the task queue.
- The three-phase order (synchronous code, microtasks, tasks) is standard; where
  runtime-specific extra queues sit is not portable.

#### Next Step

Now that the queue rule is established, we can return to the most common tool that puts
work in the queue: timers. The next lesson shows why the number inside `setTimeout` is
a lower bound rather than a promise, what order equal-delay timers enter, and how
interval scheduling drifts once its own callback runs long. The stream's first concrete
piece — a fake station driven by a timer — will be built there.

### Timers

Source: https://academia.sh/en/courses/asynchronous-javascript/timers

The delay as a lower bound, the order of equal-delay timers, overlap in interval scheduling, the self-scheduling loop, and cancellation.

The previous lesson showed that calls placing work in the queue only register it. The
most commonly used registration tool is timers, and the number given to them is often
misread: the call `setTimeout(f, 10)` does not say `f` will run in ten milliseconds.

This lesson establishes what that number actually says, what order equal-delay timers
enter, and where interval scheduling drifts. The stream's first running piece will also
appear here, as a fake station driven by a timer.

#### Delay Is a Lower Bound

The call `setTimeout(callback, delay)` means: *the callback is not queued until the
elapsed time reaches `delay`.* Once it is queued, when it actually runs depends on how
long the work ahead of it takes.

Timers registered at the same moment enter the queue by their delay; if their delays
are equal, they keep their registration order.

```js
setTimeout(() => console.log("4 — 20 ms"), 20);
setTimeout(() => console.log("3 — 10 ms"), 10);
setTimeout(() => console.log("1 — 0 ms, first registration"), 0);
setTimeout(() => console.log("2 — 0 ms, second registration"), 0);
```

```
1 — 0 ms, first registration
2 — 0 ms, second registration
3 — 10 ms
4 — 20 ms
```

A zero delay does not mean "right now," it means "at the earliest opportunity." Host
environments also apply a floor delay on top of this: the HTML standard raises the
lower bound to four milliseconds for timers nested more than five deep; server
runtimes commonly round a zero delay up to one millisecond too. Whatever the number,
nothing is promised beyond the lower bound.

#### Blocking Exceeds the Lower Bound

The lower bound has no upper bound. If the work in the previous turn runs long, a timer
whose duration has long since elapsed still waits in the queue.

```js
function busyWait(duration) {
  const deadline = Date.now() + duration;
  while (Date.now() < deadline) {
    // Deliberately keeps the thread busy.
  }
}

setTimeout(() => {
  console.log("1 — 10 ms timer; holds the thread for 50 ms");
  busyWait(50);
  console.log("2 — 10 ms timer done");
}, 10);

setTimeout(() => console.log("3 — 20 ms timer"), 20);
```

```
1 — 10 ms timer; holds the thread for 50 ms
2 — 10 ms timer done
3 — 20 ms timer
```

The second timer's duration elapsed while the first callback was still running. Under
the run-to-completion rule, it could not cut in; it waited for the first piece of work
to finish. This is the only reason a measured delay can turn out larger than the
requested one: the thread was busy.

#### Interval Scheduling and Overlap

`setInterval(callback, interval)` places the callback in the queue at regular
intervals. The interval is measured by the times *of registration*, not by the time
*after the callback finishes*. If the callback takes longer than the interval,
consecutive runs leave no gap between them.

The two programs below differ by only one line. In the first, the callback is short:

```js
let tick = 0;
const id = setInterval(() => {
  tick += 1;
  console.log("interval tick", tick);
  if (tick === 3) clearInterval(id);
}, 5);

setTimeout(() => console.log("30 ms timer"), 30);
```

```
interval tick 1
interval tick 2
interval tick 3
30 ms timer
```

Three ticks completed at five-millisecond intervals, and the timer at the thirtieth
millisecond entered the queue after. In the second, each tick does twenty milliseconds
of work:

```js
function busyWait(duration) {
  const deadline = Date.now() + duration;
  while (Date.now() < deadline) {
    // Deliberately keeps the thread busy.
  }
}

let tick = 0;
const id = setInterval(() => {
  tick += 1;
  console.log("interval tick", tick);
  busyWait(20);
  if (tick === 3) clearInterval(id);
}, 5);

setTimeout(() => console.log("30 ms timer"), 30);
```

```
interval tick 1
interval tick 2
30 ms timer
interval tick 3
```

The order changed. The second tick started before the thirtieth millisecond arrived and
held the thread for twenty milliseconds; the timer could only run once that work
finished. The third tick then came right after, with almost no pause.

This behavior has two consequences. First, in a flow built with `setInterval`, a rest
period between callbacks is not guaranteed. Second, if asynchronous work is started
inside the callback, a new run can start before the previous one finishes, and the two
pieces of work overlap.

#### The Self-Scheduling Loop

If at least one interval's worth of gap between every run is wanted, repetition is set
up with `setTimeout`: the callback registers the next one itself, once it has finished
its own work.

This pattern is the measurement stream's first running form. The fake stations' delays
and values are read from a fixed table, so the program gives the same output on every
run.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
};

function startStream(stations, interval, callback) {
  let count = 0;
  let id = null;
  let stopped = false;

  function tick() {
    const station = stations[count % stations.length];
    count += 1;
    callback({ order: count, station, value: SOURCE[station].value });
    if (!stopped) id = setTimeout(tick, interval);
  }

  id = setTimeout(tick, interval);

  return function stop() {
    stopped = true;
    clearTimeout(id);
  };
}

const stop = startStream(["A1", "B2", "C3"], 5, (measurement) => {
  console.log(measurement.order, measurement.station, measurement.value);
  if (measurement.order === 4) stop();
});
```

```
1 A1 21.4
2 B2 19.8
3 C3 23.1
4 A1 21.4
```

The `stopped` flag looks unnecessary; it is not. The `stop` function is called *from
inside* the callback, and at that moment the next tick has not yet been registered. If
only `clearTimeout` were called, the call would cancel a timer that has already run,
and `tick` would immediately register a new one right after — the stream would never
actually stop. In self-scheduling loops, cancellation has to be set up together with a
status flag.

#### A Pending Timer Keeps the Program Alive

A pending timer tells the host environment "there is work left to do." A server
runtime does not shut down until no pending work remains.

```js
process.on("exit", () => console.log("3 — runtime shutting down"));

setTimeout(() => console.log("2 — pending timer ran"), 50);
console.log("1 — script's synchronous part finished");
```

```
1 — script's synchronous part finished
2 — pending timer ran
3 — runtime shutting down
```

If the same program is cancelled with `clearTimeout`, shutdown happens immediately:

```js
process.on("exit", () => console.log("2 — runtime shutting down"));

const id = setTimeout(() => console.log("this line never runs"), 50);
clearTimeout(id);
console.log("1 — script's synchronous part finished");
```

```
1 — script's synchronous part finished
2 — runtime shutting down
```

This observation will be useful in two places later: an uncancelled timer both delays
the program's shutdown and keeps every value its callback closes over alive in memory.
The second is one of the leak types covered in the memory topic.

#### What a Timer Is Not

There are three common misreadings, and all three come from the same source.

`setTimeout(f, 0)` does not mean "run right now"; it means "run once the stack is
empty." This is why it can be used to split a long piece of work in two, but it does
not speed the work up.

A timer is not a stopwatch. The timer delay cannot be relied on to measure the time
elapsed inside a callback; measurement is done with a separate tool.

A timer is not a concurrency tool either. Two callbacks never run at the same time; one
does not start before the other finishes. Waits overlap, computations do not.

#### Summary

- A timer's delay is a lower bound; there is no upper bound, and a busy thread freely
  exceeds it.
- Equal-delay timers keep their registration order.
- `setInterval` gives no guarantee of a gap between runs; ticks overlap under
  long-running callbacks.
- A self-scheduling `setTimeout` loop leaves at least one interval's gap between every
  turn; canceling it requires a status flag.
- A pending timer keeps the runtime alive and keeps the values its callback closes over
  in memory.

#### Next Step

Timers bound a callback to time; the next step is binding a callback to a *result*.
The next lesson turns the measurement source into a callback interface that can report
an error; it tries reading three stations in sequence and together, then shows this
approach's limits: nesting, repetition of error paths, and the uncertainty that comes
from leaving control to the calling side. Promises will be born as the answer to these
limits.

### Callbacks

Source: https://academia.sh/en/courses/asynchronous-javascript/callbacks

The error-first callback contract, setting up sequential and combined processing by hand, the nesting problem, and the uncertainties that come from handing control to the callee.

Timers bound the callback to time. In a real source, what is bound is not time but a
result: notify once the measurement is ready, report why if not. This is asynchronous
programming's first widespread interface, and understanding promises requires first
seeing this interface's limits.

This lesson turns the measurement source into a callback-based interface, reads three
stations first in sequence and then together, and shows where this approach falls
short.

#### The Error-First Contract

An asynchronous function cannot hand back its result with `return`, because the result
does not exist yet when the function returns. Instead, it hands the result over through
a **callback**.

The error has to come through the same path: as seen in the previous lesson, an error
thrown from inside a callback does not reach the caller's `try` block. The established
fix is to set aside the callback's first parameter for the error — the **error-first
callback** contract.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
};

function fetchMeasurement(station, callback) {
  const record = SOURCE[station];
  if (record === undefined) {
    setTimeout(() => callback(new Error(`unknown station: ${station}`)), 0);
    return;
  }
  setTimeout(() => callback(null, { station, value: record.value }), record.delay);
}

fetchMeasurement("A1", (error, measurement) => {
  if (error) {
    console.log("A1 error:", error.message);
    return;
  }
  console.log("A1 measurement:", measurement.value);
});

fetchMeasurement("Z9", (error, measurement) => {
  if (error) {
    console.log("Z9 error:", error.message);
    return;
  }
  console.log("Z9 measurement:", measurement.value);
});

console.log("requests registered");
```

```
requests registered
Z9 error: unknown station: Z9
A1 measurement: 21.4
```

Three details are part of the contract. First, even on the error path, the callback is
called asynchronously; even the unknown-station error was left to the next turn with
`setTimeout`. Second, when there is an error, the second parameter is meaningless.
Third, error checking is written by hand at every call site — a cost repeated at the
end of the lesson.

#### Sequential Processing and Nesting

Suppose you want to read three stations in sequence and compute the average. Because
each step's result is only available inside the next step's callback, the steps nest
inside one another.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
};

function fetchMeasurement(station, callback) {
  const record = SOURCE[station];
  if (record === undefined) {
    setTimeout(() => callback(new Error(`unknown station: ${station}`)), 0);
    return;
  }
  setTimeout(() => callback(null, { station, value: record.value }), record.delay);
}

fetchMeasurement("A1", (errorA, a) => {
  if (errorA) {
    console.log("error:", errorA.message);
    return;
  }
  fetchMeasurement("B2", (errorB, b) => {
    if (errorB) {
      console.log("error:", errorB.message);
      return;
    }
    fetchMeasurement("C3", (errorC, c) => {
      if (errorC) {
        console.log("error:", errorC.message);
        return;
      }
      const average = (a.value + b.value + c.value) / 3;
      console.log("order:", [a.station, b.station, c.station].join(" -> "));
      console.log("average:", average.toFixed(2));
    });
  });
});
```

```
order: A1 -> B2 -> C3
average: 21.43
```

The program is correct, but three observations can be made. Indentation grows with the
number of steps, not with logical complexity; ten steps means ten times the
indentation. Error checking is repeated three times, identically each time. Last, the
code that will use the flow's result stays at the innermost level; adding a step
requires touching everything from the outermost level to the innermost.

This layout's common name is the callback pyramid. The real problem is not its
appearance, it is that it is **not composable**: there is no general operator that
combines two asynchronous steps; every combination is written by hand.

#### Combined Processing: The Cost of Coordination

If the three stations are independent of each other, waiting for them in sequence is
unnecessary; all three can be started together. In this case, gathering the results
requires a coordinating function that keeps a counter and a flag.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
};

function fetchMeasurement(station, callback) {
  const record = SOURCE[station];
  if (record === undefined) {
    setTimeout(() => callback(new Error(`unknown station: ${station}`)), 0);
    return;
  }
  setTimeout(() => callback(null, { station, value: record.value }), record.delay);
}

function fetchAll(stations, callback) {
  const results = new Array(stations.length);
  let remaining = stations.length;
  let done = false;

  stations.forEach((station, index) => {
    fetchMeasurement(station, (error, measurement) => {
      if (done) return;
      if (error) {
        done = true;
        callback(error);
        return;
      }
      console.log("arrived:", station);
      results[index] = measurement;
      remaining -= 1;
      if (remaining === 0) {
        done = true;
        callback(null, results);
      }
    });
  });
}

fetchAll(["A1", "B2", "C3"], (error, measurements) => {
  if (error) {
    console.log("error:", error.message);
    return;
  }
  console.log("result order:", measurements.map((m) => m.station).join(" "));
});
```

```
arrived: B2
arrived: C3
arrived: A1
result order: A1 B2 C3
```

Arrival order followed the delays — B2, C3, A1 — yet the result array kept the request
order. What makes this possible is placing results by index; using position instead of
relying on order is the basic pattern in asynchronous aggregation.

The `done` flag is also necessary: it stops results arriving after an error from
triggering the callback a second time. So a correct "wait for all" function keeps two
separate pieces of state — a counter and a flag. This job will move into the library
along with promises and shrink to a single line.

#### Handing Control to the Callee

When you give a callback, you do not decide when and how many times your function will
be called; the code you called decides. This is called **inversion of control**, and it
produces three concrete uncertainties.

**The callback might never be called.** If the source does not respond, nothing
happens; the program sees neither a result nor an error, it just silently waits. A
timeout has to be set up separately to close this gap.

**The callback might be called more than once.** The contract forbids this, but the
language does not enforce it.

```js
function buggyFetchMeasurement(station, callback) {
  setTimeout(() => {
    callback(null, { station, value: 21.4 });
    callback(null, { station, value: 21.4 });
  }, 5);
}

let count = 0;
let total = 0;

buggyFetchMeasurement("A1", (error, measurement) => {
  count += 1;
  total += measurement.value;
  console.log("count:", count, "total:", total.toFixed(1));
});
```

```
count: 1 total: 21.4
count: 2 total: 42.8
```

A single measurement was counted twice. The fault is in the source, not the side doing
the counting; but the side doing the counting bears the consequence.

**The callback can sometimes be called synchronously.** Calling the callback directly
on a cache hit, on the thinking that there is no need to wait, changes the flow's
order.

```js
const SOURCE = { B2: { delay: 10, value: 19.8 } };

function fetchMeasurement(station, callback) {
  const record = SOURCE[station];
  setTimeout(() => callback(null, { station, value: record.value }), record.delay);
}

const cache = new Map();

function cachedFetch(station, callback) {
  if (cache.has(station)) {
    callback(null, cache.get(station));
    return;
  }
  fetchMeasurement(station, (error, measurement) => {
    cache.set(station, measurement);
    callback(error, measurement);
  });
}

let status = "preparing";

cachedFetch("B2", (error, measurement) => {
  console.log("1 — first call, status:", status);
  status = "processing";
  cachedFetch("B2", () => {
    console.log("2 — call from cache, status:", status);
  });
  status = "after inner call";
  console.log("3 — line after the inner call, status:", status);
});

status = "ready";
```

```
1 — first call, status: ready
2 — call from cache, status: processing
3 — line after the inner call, status: after inner call
```

The second call's callback ran **before** the line right after it, because it was
called synchronously on the cache hit. Same interface, same call, two different
orders. The rule is clear: an interface should not call its callback sometimes
synchronously and sometimes asynchronously. Even on a cache hit, the result should be
left for the next turn.

#### The Gap Callbacks Leave

The programs in this lesson run and none of them contains a language error. Yet all of
them share the same gaps: the error path is written by hand at every level,
`try`/`catch` is useless, there is no general operator for combining steps, and
contract violations (never calling, calling twice, calling synchronously) are not
prevented by the language.

The result is that an asynchronous operation is not a **first-class value**. A callback
notifies the side that gets the result; but there is no object representing the
operation itself — one that can be stored, returned, or handed to someone else.

#### Summary

- An asynchronous function cannot return its result; the error-first callback contract
  delivers both the result and the error through a single path.
- Sequential steps nest inside callbacks; indentation and error checking grow with the
  number of steps.
- Gathering requests that run together requires a counter and a flag; results are
  placed by position, not by arrival order.
- Inversion of control produces three uncertainties: the callback never being called,
  being called more than once, and sometimes being called synchronously.
- In the callback arrangement, an asynchronous operation is not a first-class value;
  this is why it cannot be composed.

#### Next Step

This gap's answer is representing "an operation whose result is not yet known" as a
value. The next lesson defines that value: the promise. A promise's three states, states
being able to change only once, callbacks being taken into a microtask, and chaining
producing a new promise at every step — three of this lesson's four gaps will follow
directly from
these rules.

## Promises and async/await

A promise's state machine and chaining rules, waiting in parallel with combinators, the async/await syntactic layer, asynchronous error propagation, and asynchronous iteration.

### The Promise Concept

Source: https://academia.sh/en/courses/asynchronous-javascript/promise-concept

A promise's three-state machine, the result being decided exactly once, callbacks being deferred to a microtask, and each step of chaining producing a new promise.

The previous lesson closed with a gap: in the callback pattern, an asynchronous
operation is not a value. It cannot be stored, returned, or handed to someone else.
Closing that gap requires an object that represents "an operation whose result is not
yet known."

This object is called a **promise**. This lesson builds the promise's state machine, how
it is produced, and the rules of chaining; the measurement source also moves, in this
lesson, from a callback interface to a promise interface.

#### The Three-State Machine

A promise is in exactly one of three states:

| State | Meaning |
|---|---|
| pending | The result is not yet known |
| fulfilled | Settled with a value |
| rejected | Settled with a reason |

Fulfilled and rejected share a common name: the **settled** state. Transitions are
one-way: a pending promise can settle, but a settled promise never returns to pending
and can never change its state.

These two constraints directly remove two of the ambiguities from the previous lesson:
the result cannot be reported twice, because the second report is ignored; and it cannot
be changed after being reported, because the state is permanent.

#### Producing a Promise

A function that returns a promise starts the work and immediately returns the object
representing the result. The function given to the constructor is called the
**executor**; it receives two functions: one that settles the result, one that rejects
it.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    if (record === undefined) {
      setTimeout(() => reject(new Error(`unknown station: ${station}`)), 0);
      return;
    }
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

requestMeasurement("B2").then(
  (measurement) => console.log("fulfilled:", measurement.station, measurement.value),
  (error) => console.log("rejected:", error.message),
);

requestMeasurement("D4").then(
  (measurement) => console.log("fulfilled:", measurement.station, measurement.value),
  (error) => console.log("rejected:", error.message),
);

console.log("both promises are pending");
```

```
both promises are pending
fulfilled: B2 19.8
rejected: D4: sensor fault
```

A faulty station was added to the source table; `D4` rejects on every call. This
station will be used to test error paths for the rest of the course.

The executor's body runs **synchronously**; the timer is set up before the
`requestMeasurement` call even returns. The returned value is a pending promise. This is
why the message on the last line is printed first.

One point deserves attention: the work starts **when the promise is created**. A
promise represents not "work to be started" but "the result of work that has already
started." The next lesson shows the practical consequence of this distinction.

#### The Result Is Decided Exactly Once

The state machine's permanence is guaranteed by the language itself. However many times
a decision is made inside the executor, the **first** one holds.

```js
const promise = new Promise((resolve, reject) => {
  resolve("first decision");
  resolve("second decision");
  reject(new Error("late rejection"));
});

promise.then(
  (value) => console.log("value:", value),
  (error) => console.log("error:", error.message),
);
```

```
value: first decision
```

The second decision and the rejection were silently ignored. The job that the
hand-written `done` flag did in the callback pattern is done here by the language
itself.

#### Callbacks Are Always a Microtask

Even if a promise has already settled, its `then` callback does not run immediately; it
is placed in the **microtask queue**. This removes the "sometimes synchronous, sometimes
asynchronous" ambiguity from the previous topic.

```js
const promise = Promise.resolve("ready value");

promise.then((value) => console.log("2 — first then:", value));
promise.then((value) => console.log("3 — second then:", value));
console.log("1 — synchronous");
queueMicrotask(() => console.log("4 — microtask registered after"));
```

```
1 — synchronous
2 — first then: ready value
3 — second then: ready value
4 — microtask registered after
```

The promise produced by `Promise.resolve` was already fulfilled the moment it was
written; even so, its callbacks ran only after all the synchronous code had run. More
than one `then` can be attached to the same promise, and all of them run, in
registration order, in the same microtask drain.

This rule has a side effect too: since promise callbacks are microtasks, a long chain of
promises calling one another can keep the task queue waiting. The starvation warning
from the previous topic applies to promises as well.

#### Chaining: Every Step Produces a New Promise

A `then` call does not change the promise; it returns a **new** promise. The new
promise's result depends on what the callback returns:

- If it returns an ordinary value, the new promise fulfills with that value.
- If it returns a promise, the new promise waits for that promise's result and settles
  with it.
- If it throws, the new promise rejects with that error.

A rejection flows down the chain until it finds a callback that handles it. `catch` is
just a form of `then` that handles rejection only; `finally` runs in both cases without
changing the result.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
};

function requestMeasurement(station) {
  return new Promise((resolve) => {
    const record = SOURCE[station];
    setTimeout(() => resolve({ station, value: record.value }), record.delay);
  });
}

requestMeasurement("B2")
  .then((measurement) => {
    console.log("1 — raw measurement:", measurement.value);
    return measurement.value;
  })
  .then((value) => {
    if (value > 25) throw new Error("value out of range");
    return Math.round(value);
  })
  .then((rounded) => {
    console.log("2 — rounded:", rounded);
    return requestMeasurement("A1");
  })
  .then((measurement) => {
    console.log("3 — chain's second measurement:", measurement.station, measurement.value);
    throw new Error("error in processing step");
  })
  .catch((error) => {
    console.log("4 — caught:", error.message);
    return "recovery value";
  })
  .then((value) => console.log("5 — chain continues after catch:", value))
  .finally(() => console.log("6 — finally runs in every case"));
```

```
1 — raw measurement: 19.8
2 — rounded: 20
3 — chain's second measurement: A1 21.4
4 — caught: error in processing step
5 — chain continues after catch: recovery value
6 — finally runs in every case
```

The third step returned a promise, and the chain waited for it to settle; the error in
the fourth step reached `catch`; since `catch` returned a value, the chain returned to
the fulfilled state and continued.

This last behavior produces a common misconception: `catch` **swallows** the error and
repairs the chain. If the error is meant to propagate, it has to be re-thrown inside
`catch`.

Any object whose result is a promise can join this chain. The standard accepts, for
this purpose, any object that has a method named `then`; such objects are called
**thenables**. When one enters the chain, it is treated exactly like a real promise.

#### What Changed Compared to Callbacks

Three of the previous lesson's four gaps are directly closed. The result is now a
**value**: it can be stored, returned, put in an array. The error path can be written
for the entire chain with a single `catch`. Contract violations are prevented by the
language itself — a decision cannot be made twice, and a callback cannot be called
synchronously.

The one gap that stays open is the possibility that the operation never settles at
all. A promise stuck in pending can stay pending forever; the mechanism that solves
this is a timeout, and it will be built in a later lesson.

#### Summary

- A promise is a value representing an operation whose result is not yet known; it has
  three states, and its state does not change once settled.
- The work starts when the promise is created; a promise represents the result of work
  that has already started.
- The executor runs synchronously, but `then` callbacks are always deferred to a
  microtask.
- `then` returns a new promise; returning a value continues the chain, returning a
  promise makes it wait, throwing sends a rejection down the chain.
- `catch` handles rejection and returns the chain to the fulfilled state; if the error
  is meant to continue, it has to be re-thrown.

#### Next Step

Now that a single promise is a value, multiple promises can be treated as a
collection. The next lesson introduces the combinators that wait for three stations
together, take the first response, or pick the first successful response; the
coordination function hand-written with a counter and a flag in the previous topic
comes down to a single line. The same lesson also compares the combinators' different
behavior in the face of errors.

### Promise Combinators

Source: https://academia.sh/en/courses/asynchronous-javascript/promise-combinators

Four combinators that gather multiple promises into a single promise; their differing behavior in the face of errors; the distinction between when work starts and when it is awaited.

The previous lesson turned a single promise into a value. What is a value can be put
in an array; what is put in an array can be handled together. The "wait for all"
function hand-written with a counter and a flag in the callbacks topic can now be
written with an operator the language provides.

This lesson introduces four combinators and shows that the real difference between
them is their behavior **in the face of errors**. The measurement stream turns here
into an aggregator that reads three stations together.

#### When Work Starts

Reading the combinators correctly requires clearing up a misunderstanding first:
**combinators do not start work.** The work has already started once the promises are
created; the combinator only organizes the **waiting**.

In the examples below, the measurement source also prints the moment the request
starts. The table used throughout the course also includes the faulty `D4` station.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  console.log("request started:", station);
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

const measurements = await Promise.all([requestMeasurement("A1"), requestMeasurement("B2"), requestMeasurement("C3")]);
console.log("result:", measurements.map((m) => `${m.station}=${m.value}`).join(" "));
```

```
request started: A1
request started: B2
request started: C3
result: A1=21.4 B2=19.8 C3=23.1
```

All three requests started before `Promise.all` was even called, while the array was
being prepared. The waits **overlapped**; the result arrived once the slowest request,
A1, finished.

The result array preserves **request order**, not arrival order. In the callbacks
topic, achieving this required placing results by index; here, the guarantee belongs
to the combinator itself.

#### Wait for All: `Promise.all`

`Promise.all` fulfills, with a result array, once **all** of the given promises have
fulfilled. If any one of them rejects, it rejects **immediately, with the first
rejection**.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  console.log("request started:", station);
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

try {
  await Promise.all([requestMeasurement("A1"), requestMeasurement("D4"), requestMeasurement("C3")]);
} catch (error) {
  console.log("all rejected:", error.message);
}
```

```
request started: A1
request started: D4
request started: C3
all rejected: D4: sensor fault
```

Two points matter. First, the result was known at the fifteenth millisecond; A1 and C3
were never waited for. Second, and more important: **not being waited for is not being
canceled.** The A1 and C3 requests keep going, and their results are still produced; no
one just looks at them. Cancellation requires a separate mechanism and will be built
in a later lesson.

This behavior is correct for jobs meaning "all or nothing": the average of three
measurements cannot be computed while one is missing.

#### Collect Every Result: `Promise.allSettled`

When a partial result is acceptable, `all` is the wrong tool. `Promise.allSettled`
**never rejects**; it returns a record carrying the status and result of every
promise.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  console.log("request started:", station);
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

const results = await Promise.allSettled([requestMeasurement("A1"), requestMeasurement("D4"), requestMeasurement("C3")]);
for (const r of results) {
  if (r.status === "fulfilled") console.log("fulfilled:", r.value.station, r.value.value);
  else console.log("rejected:", r.reason.message);
}
```

```
request started: A1
request started: D4
request started: C3
fulfilled: A1 21.4
rejected: D4: sensor fault
fulfilled: C3 23.1
```

The faulty station did not stop the stream; two valid measurements were obtained, and
the error was recorded too. In areas like a measurement network, where partial data is
useful, this is the default choice.

The records' field names are fixed: a fulfilled record has `status` and `value`; a
rejected record has `status` and `reason`.

#### The First to Settle: `Promise.race`

`Promise.race` takes the result of whichever promise settles first — it makes no
distinction between fulfillment and rejection.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  console.log("request started:", station);
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

const first = await Promise.race([requestMeasurement("A1"), requestMeasurement("B2"), requestMeasurement("C3")]);
console.log("race result:", first.station, first.value);

try {
  await Promise.race([requestMeasurement("A1"), requestMeasurement("D4")]);
} catch (error) {
  console.log("race rejected:", error.message);
}
```

```
request started: A1
request started: B2
request started: C3
race result: B2 19.8
request started: A1
request started: D4
race rejected: D4: sensor fault
```

In the second race, the promise that settled fastest was the faulty station's; `race`
counted it as the result too. This is why `race` does not mean "take the fastest
**healthy** response."

One warning is needed: promises that lose the race **keep living**. If a promise that
never settles is put inside `race`, that promise and everything it closes over stays
in memory for the life of the program. This is one of the leak types covered in the
memory topic.

#### The First to Succeed: `Promise.any`

`Promise.any` ignores rejections and returns the result of the **first to fulfill**. If
all of them reject, it rejects with an aggregate error containing all the reasons.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  console.log("request started:", station);
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

const firstSuccessful = await Promise.any([requestMeasurement("A1"), requestMeasurement("D4")]);
console.log("any result:", firstSuccessful.station, firstSuccessful.value);

try {
  await Promise.any([requestMeasurement("D4")]);
} catch (error) {
  console.log("any rejected:", error.constructor.name);
  console.log("errors inside:", error.errors.map((e) => e.message).join(", "));
}
```

```
request started: A1
request started: D4
any result: A1 21.4
request started: D4
any rejected: AggregateError
errors inside: D4: sensor fault
```

The faulty station rejected at the fifteenth millisecond; `any` ignored it and waited
for the valid response at the thirtieth millisecond. The aggregate error object
carries the individual reasons in an `errors` array — so no error information is lost.

This combinator is the direct counterpart of the redundancy pattern that requests the
same data from multiple sources and uses whichever works first.

#### The Selection Criterion

| Combinator | Fulfills | Rejects | Fits when |
|---|---|---|---|
| `all` | If all fulfill | On the first rejection | Every part is required |
| `allSettled` | Always | Never | A partial result is acceptable |
| `race` | If the first to settle fulfills | If the first to settle rejects | Setting a time limit |
| `any` | On the first to fulfill | If all reject | Redundant sources |

All four take the same input shape and return a single promise; this is why they can
be **nested**. A result chosen with `any` from one set of stations can be combined
with another set's result inside `all`.

#### Summary

- Combinators do not start work; the work has started once the promises are created,
  and the combinator only organizes the waiting.
- `all` gives the result array in request order and rejects immediately on the first
  rejection; requests that are not waited for are not canceled, they keep going.
- `allSettled` never rejects; it returns a status-and-result record for every promise.
- `race` looks at whichever settles first and does not distinguish success from
  failure; `any` picks the first to fulfill and, if all reject, carries the reasons in
  an aggregate error.
- Promises that lose the race and never settle continue to occupy memory.

#### Next Step

The combinators built the structure of waiting, but they did not change how the code
reads: results are still processed inside callbacks. The next lesson introduces the
syntactic layer that expresses the same chains with sequential-looking syntax —
`async` and `await`. The same lesson shows the common bug where this syntax leads to
unintentional sequential waiting, without the writer noticing, and fixes it with the
combinators built in this lesson.

### async/await

Source: https://academia.sh/en/courses/asynchronous-javascript/async-await

An async function always returning a promise, await suspending the function, the syntactic layer's correspondence to a promise chain, and the bug of falling into unintentional sequential waiting.

Promises turned an asynchronous operation into a value, and combinators organized the
waiting. Even so, how the code reads did not change: every step's result is still
processed inside a callback, and the flow does not run top to bottom in the source
text.

This lesson builds the layer that expresses the same promise chains with
sequential-looking syntax. The point to emphasize should be said up front: `async` and
`await` do not introduce a new asynchrony mechanism. Both are a **syntactic layer**
built on top of promises; the event loop's rules apply exactly as they are.

#### `async` Always Returns a Promise

A function defined with the `async` keyword returns a promise no matter what it
returns. If it returns an ordinary value, the promise fulfills with that value.

```js
async function fixedMeasurement() {
  return { station: "A1", value: 21.4 };
}

const returned = fixedMeasurement();
console.log("is the returned value a promise:", returned instanceof Promise);
console.log("awaited result:", await returned);
```

```
is the returned value a promise: true
awaited result: { station: 'A1', value: 21.4 }
```

The reverse is true too: an error thrown inside an async function's body does not
propagate outward; it **rejects** the returned promise. The details of error
propagation are the next lesson's subject.

#### `await` Suspends the Function, Not the Thread

`await` waits for a promise to settle. While waiting, it suspends **only the function
it is inside**; the thread stays free and control returns to the calling code.

The observable consequence of this is: an async function's body runs **synchronously
up to its first `await`**; everything after that turns into a microtask.

```js
async function processMeasurement() {
  console.log("2 — async body starts synchronously");
  await null;
  console.log("4 — continues in a microtask after the first await");
}

console.log("1 — before the call");
processMeasurement();
console.log("3 — after the call");
```

```
1 — before the call
2 — async body starts synchronously
3 — after the call
4 — continues in a microtask after the first await
```

The awaited value was not even a promise; `await null` still adds a microtask turn.
Formally, the rule is: an `await` expression wraps the awaited value with
`Promise.resolve` and registers what follows as that promise's callback.

Two async functions can advance **side by side**, not nested inside one another. Every
`await` hands control back to the event loop, and the other function's next step
slots in between:

```js
async function firstFlow() {
  console.log("A — step 1");
  await null;
  console.log("A — step 2");
  await null;
  console.log("A — step 3");
}

async function secondFlow() {
  console.log("B — step 1");
  await null;
  console.log("B — step 2");
  await null;
  console.log("B — step 3");
}

firstFlow();
secondFlow();
console.log("synchronous part finished");
```

```
A — step 1
B — step 1
synchronous part finished
A — step 2
B — step 2
A — step 3
B — step 3
```

The two flows interleaved step by step; even so, the two never ran at the same
instant. The single-threaded model was not broken — only the split points were
explicitly marked with `await` expressions.

#### The Chain's Counterpart

The same flow can be expressed with either syntax; the promise and the result they
produce are identical.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  console.log("request started:", station);
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

async function getAverage() {
  const a = await requestMeasurement("A1");
  const b = await requestMeasurement("B2");
  return (a.value + b.value) / 2;
}

function getAverageChained() {
  return requestMeasurement("A1").then((a) =>
    requestMeasurement("B2").then((b) => (a.value + b.value) / 2),
  );
}

console.log("await syntax:", (await getAverage()).toFixed(2));
console.log("chain syntax:", (await getAverageChained()).toFixed(2));
```

```
request started: A1
request started: B2
await syntax: 20.60
request started: A1
request started: B2
chain syntax: 20.60
```

The difference between the two forms is **readability**. In `await` syntax,
intermediate results are ordinary local variables; reaching the first step's result
in the second step requires no nesting. The difference grows as the number of steps
grows: in chain syntax, every added step demands either a new indentation level or a
hand-carried intermediate object.

#### The Sequential-Wait Trap

`await` gives a sequential-looking reading, and that reading can be misleading: waits
written one after another actually **run** one after another.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  console.log("request started:", station);
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

for (const station of ["A1", "B2", "C3"]) {
  const measurement = await requestMeasurement(station);
  console.log("arrived:", measurement.station, measurement.value);
}
```

```
request started: A1
arrived: A1 21.4
request started: B2
arrived: B2 19.8
request started: C3
arrived: C3 23.1
```

The output's order gives the diagnosis: every request starts only **after** the
previous one's result arrives. Even though the three stations are independent of each
other, the waits do not overlap; the total time equals the sum of the three delays.

The fix is to separate **starting** the requests from **waiting** for them. Since the
work starts once the promises are created, all of them are created first, then
awaited at a single point.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  console.log("request started:", station);
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

const promises = ["A1", "B2", "C3"].map((station) =>
  requestMeasurement(station).then((measurement) => {
    console.log("arrived:", measurement.station, measurement.value);
    return measurement;
  }),
);
const measurements = await Promise.all(promises);
console.log("total:", measurements.reduce((t, m) => t + m.value, 0).toFixed(1));
```

```
request started: A1
request started: B2
request started: C3
arrived: B2 19.8
arrived: C3 23.1
arrived: A1 21.4
total: 64.3
```

All three requests started immediately; the results arrived in order of delay. Total
wait equals not the sum of the three delays but the **largest** one.

The criterion is a simple question: **does this step genuinely depend on the
previous one's result?** If the answer is no, an `await` inside a loop is a bug. If
the answer is yes — for example, if the second station's identity comes out of the
first measurement — sequential waiting is correct and necessary.

#### Top-Level `await`

At the module level, `await` can be used without being inside a function. The last
lines of this lesson's examples use exactly this. The details tied to module forms
are the Modules, Tooling and the Ecosystem course's subject; the only thing that
needs to be known here is that a top-level await **delays the module's evaluation**.

#### Summary

- An async function always returns a promise; an error thrown in its body rejects the
  returned promise.
- `await` suspends only its own function; the thread stays free, and control returns
  to the calling code.
- The body runs synchronously up to the first `await`; what follows turns into a
  microtask — even awaiting a non-promise value adds a turn.
- `await` syntax and chain syntax produce the same promise; the difference is
  readability.
- Using `await` inside a loop for independent jobs serializes the waits; the correct
  pattern is to start the jobs and await them at a single point.

#### Next Step

This lesson deliberately left the error path aside. The next lesson takes it up:
where an error thrown in an async body goes, when `try`/`catch` and `catch` syntax
diverge, in which case the combinators lose error information, and how a rejection no
one handles gets reported to the runtime. The measurement stream will also become
resilient to the faulty station there.

### Asynchronous Error Handling

Source: https://academia.sh/en/courses/asynchronous-javascript/asynchronous-error-handling

Rejection propagating through the chain, the points where try/catch and catch syntax diverge, the loss of error information, the cause chain, and tracking unhandled rejections.

The previous lesson deliberately left the error path aside. But in asynchronous code,
an error behaves differently than in synchronous code: since the stack has long since
emptied, there is no caller left for the error to throw "upward" to. A rejection is
carried not through the stack but through the **promise itself**.

This lesson establishes how rejection propagates, which syntax catches which error,
and what a rejection that no one handles actually is. The measurement stream becomes
resilient to faulty stations here.

#### Rejection Is Caught with `try`/`catch`

If a promise awaited with `await` rejects, it is treated as if an error were thrown at
the point of waiting. This is the single rule that makes the `try`/`catch`/`finally`
structure reusable in asynchronous code.

A second faulty station was added to the source table; `E5` rejects with a shorter
delay. This will be used in examples where the order of errors matters.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
  E5: { delay: 5, error: "no connection" },
};

function requestMeasurement(station) {
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

try {
  const measurement = await requestMeasurement("D4");
  console.log("this line does not run:", measurement.value);
} catch (error) {
  console.log("caught:", error.message);
} finally {
  console.log("finally: resource released");
}
```

```
caught: D4: sensor fault
finally: resource released
```

The `finally` block keeps its synchronous-code meaning: it runs no matter what the
result is, and it does not change the result. Cleanup work — closing an opened
resource, canceling a set-up timer — is written here.

#### `then`'s Second Parameter Is Not the Same as `catch`

In chain syntax, an error handler can be placed in two spots, and the two catch
different things. The second callback given as `then(onSuccess, onError)` handles only
the rejection of the promise **before it**; it does not handle an error thrown by the
success callback in that same `then` call.

```js
Promise.resolve("raw value")
  .then(
    () => {
      throw new Error("error in processing step");
    },
    (error) => console.log("this callback does not run:", error.message),
  )
  .catch((error) => console.log("caught at the end of the chain:", error.message));
```

```
caught at the end of the chain: error in processing step
```

The rule is: an error handler has to come **after** the step where the error occurs.
This is why the default choice is putting a single `catch` at the end of the chain; the
two-parameter form only makes sense when the intent is "let someone else handle this
step's own error."

#### Returning by Awaiting

In an async function, `return requestMeasurement(...)` and
`return await requestMeasurement(...)` give the same result in most contexts. They do
**not** inside a `try` block.

```js
const SOURCE = {
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

async function returnWithoutAwait(station) {
  try {
    return requestMeasurement(station);
  } catch (error) {
    return "catch ran";
  }
}

async function returnWithAwait(station) {
  try {
    return await requestMeasurement(station);
  } catch (error) {
    return "catch ran";
  }
}

console.log("with await:", await returnWithAwait("D4"));
try {
  await returnWithoutAwait("D4");
} catch (error) {
  console.log("without await: error leaked out —", error.message);
}
```

```
with await: catch ran
without await: error leaked out — D4: sensor fault
```

With a promise returned without awaiting, the function returns before the promise
settles; the `try` block has already closed by then, and the rejection passes to the
caller. The rule: **if you want to catch a promise in your own `try` block, you have
to await it before returning.**

#### The Loss of Error Information

Combinators carry error information differently. `Promise.all` gives only the
**first** rejection; the rest disappear silently.

```js
const SOURCE = {
  D4: { delay: 15, error: "sensor fault" },
  E5: { delay: 5, error: "no connection" },
};

function requestMeasurement(station) {
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

try {
  await Promise.all([requestMeasurement("D4"), requestMeasurement("E5")]);
} catch (error) {
  console.log("all gives only the first error:", error.message);
}

const results = await Promise.allSettled([requestMeasurement("D4"), requestMeasurement("E5")]);
const errors = results.filter((r) => r.status === "rejected").map((r) => r.reason.message);
console.log("allSettled gives all of them:", errors.join(" | "));
```

```
all gives only the first error: E5: no connection
allSettled gives all of them: D4: sensor fault | E5: no connection
```

Both stations were faulty; `all` reported only whichever rejected earlier. In cases
where every error is needed for diagnosis — for example, logging which stations went
down — `allSettled` should be used.

#### The Cause Chain

Catching an error and throwing a more meaningful one in its place is common; but if
the root cause is lost, diagnosis gets harder. An error object's `cause` option
attaches the lower-level error to the higher-level one.

```js
const SOURCE = {
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

async function readMeasurement(station) {
  try {
    return await requestMeasurement(station);
  } catch (error) {
    throw new Error(`could not read station: ${station}`, { cause: error });
  }
}

try {
  await readMeasurement("D4");
} catch (error) {
  console.log("top-level error:", error.message);
  console.log("root cause:", error.cause.message);
}
```

```
top-level error: could not read station: D4
root cause: D4: sensor fault
```

This pattern prevents information loss while adding context between layers: the upper
layer says "which station," the lower layer says "why," and both are carried in a
single object.

#### Unhandled Rejections

If a promise rejects and no error handler is ever attached to it, the result is an
error caught nowhere. The runtime tracks this and reports it.

```js
const SOURCE = {
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

process.on("unhandledRejection", (reason) => {
  console.log("1 — unhandled rejection reported:", reason.message);
});

process.on("rejectionHandled", () => {
  console.log("3 — rejection handled later");
});

const promise = requestMeasurement("D4");
setTimeout(() => promise.catch((error) => console.log("2 — late catch:", error.message)), 50);
```

```
1 — unhandled rejection reported: D4: sensor fault
2 — late catch: D4: sensor fault
3 — rejection handled later
```

A handler was attached later, and the error really was caught; but by then the
runtime had already made its "unhandled rejection" report. The report is produced for
rejections that are still without a handler once the microtask queue drains.

This has two practical consequences. First, the error handler should be attached
**where you create** the promise; leaving it for later produces a false alarm. Second,
if no listener is registered at all, the runtime applies its default behavior: in a
server environment, a diagnostic message is printed and the process exits with a
nonzero code. This is why an unhandled rejection is not a warning to log and move
past, but a **defect to fix**.

#### Resilient Measurement Aggregation

When this lesson's tools combine, the fault-resilient form of the measurement stream
emerges: every station produces either a result or a failure with a known reason, no
error disappears silently, and aggregation does not stop because of a single fault.
The one remaining gap is a station that never responds at all; neither `catch` nor
`allSettled` solves that, because there is no rejected promise in the picture. That
gap will be closed by the timeout mechanism.

#### Summary

- Rejection is carried through the promise, not the stack; when a promise awaited
  with `await` rejects, it is treated as if an error were thrown at the point of
  waiting.
- `then`'s second parameter handles only the previous step's error; a `catch` placed
  at the end of the chain covers every step before it.
- Catching a promise in its own `try` block requires awaiting it before returning.
- `all` carries only the first rejection; `allSettled` is used when every error is
  needed.
- The `cause` option makes wrapping an error possible while preserving the root
  cause.
- An unhandled rejection is one that stays without a handler once the microtask
  queue drains, and it is reported by the runtime.

#### Next Step

Up to this point, every measurement was a one-time result: request, wait, get.
Measurement stations, though, produce data continuously; their result is represented
not by a single promise but by an open-ended **sequence**. The next lesson takes up
that sequence: the asynchronous iterator protocol, the `for await` loop, asynchronous
generators, and the backpressure problem that arises when the consumer is slower than
the producer.

### Asynchronous Iteration

Source: https://academia.sh/en/courses/asynchronous-javascript/asynchronous-iteration

The asynchronous iterator protocol, the for await loop, asynchronous generators, cleanup on early exit, and pull-based streams' advantage against backpressure.

Up to this point, every measurement was a one-time result: request, wait, get. A
station, though, produces data continuously. Its result is represented not by a single
promise but by an open-ended **sequence** — and this sequence's elements arrive over
time, one at a time.

This lesson takes up that sequence. The ordinary iteration protocol falls short here:
the next element's readiness has to be waited for, yet a `next` call is required to
return its value immediately. The solution is to extend the protocol so it returns a
promise.

#### The Asynchronous Iterator Protocol

The iteration protocol introduced in the Objects and Functions in JavaScript course
expects a `next` call to return an object carrying `value` and `done` fields. In the
asynchronous protocol, the only difference is that the `next` call returns a
**promise that fulfills** with such an object.

An object implementing the protocol defines a method named `Symbol.asyncIterator`
instead of `Symbol.iterator`.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
  E5: { delay: 5, error: "no connection" },
};

function requestMeasurement(station) {
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

const measurementSource = {
  stations: ["A1", "B2"],
  [Symbol.asyncIterator]() {
    const stations = this.stations;
    let index = 0;
    return {
      async next() {
        if (index >= stations.length) return { value: undefined, done: true };
        const measurement = await requestMeasurement(stations[index]);
        index += 1;
        return { value: measurement, done: false };
      },
    };
  },
};

for await (const measurement of measurementSource) {
  console.log("from the protocol:", measurement.station, measurement.value);
}
```

```
from the protocol: A1 21.4
from the protocol: B2 19.8
```

The `for await` loop calls `next` on every turn, awaits the returned promise, and
continues until the `done` field is true. The loop's body can be asynchronous too;
each turn waits for the previous one to finish.

#### Asynchronous Generators

Writing the protocol by hand means carrying state variables by hand. An
**asynchronous generator** takes over this job: it is defined as `async function*`,
and its body can use both `await` and `yield`.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
  E5: { delay: 5, error: "no connection" },
};

function requestMeasurement(station) {
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

async function* measurementStream(stations) {
  for (const station of stations) {
    console.log("producer: request started —", station);
    yield await requestMeasurement(station);
  }
}

for await (const measurement of measurementStream(["A1", "B2", "C3"])) {
  console.log("consumer: processed —", measurement.station, measurement.value);
}
console.log("stream finished");
```

```
producer: request started — A1
consumer: processed — A1 21.4
producer: request started — B2
consumer: processed — B2 19.8
producer: request started — C3
consumer: processed — C3 23.1
stream finished
```

The output's order shows this structure's most important trait: the producer does
not request the next item until the consumer has processed the current one. The
generator suspends at the `yield` point and continues only once a new `next` call
arrives. This is called a **pull-based** stream; the consumer sets the pace.

#### Errors and Early Exit

A rejection inside the stream is thrown at the location of the `for await` loop and
is caught with an ordinary `try`/`catch`. The caught error terminates the loop.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

async function* measurementStream(stations) {
  for (const station of stations) {
    yield await requestMeasurement(station);
  }
}

try {
  for await (const measurement of measurementStream(["A1", "D4", "C3"])) {
    console.log("consumer:", measurement.station, measurement.value);
  }
} catch (error) {
  console.log("stream stopped with an error:", error.message);
}
```

```
consumer: A1 21.4
stream stopped with an error: D4: sensor fault
```

The third station never got its turn: the generator terminated at the point where
the error was thrown. If the stream is meant to continue despite the error, the
error has to be caught **inside** the generator and converted into a result
object — this is the stream counterpart of the previous lesson's `allSettled`
approach.

Exiting the loop with `break` also terminates the generator. This gives an
opportunity to close opened resources: the generator's `finally` block runs in
either case.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
};

function requestMeasurement(station) {
  return new Promise((resolve) => {
    const record = SOURCE[station];
    setTimeout(() => resolve({ station, value: record.value }), record.delay);
  });
}

async function* measurementStream(stations) {
  try {
    for (const station of stations) {
      yield await requestMeasurement(station);
    }
  } finally {
    console.log("producer: cleanup ran");
  }
}

for await (const measurement of measurementStream(["A1", "B2", "C3"])) {
  console.log("consumer:", measurement.station);
  if (measurement.station === "B2") break;
}
console.log("exited the loop");
```

```
consumer: A1
consumer: B2
producer: cleanup ran
exited the loop
```

Cleanup ran before the loop was exited. In generators reading a continuous
resource — an open connection, a set-up timer — this block is mandatory; otherwise the
resource stays open even after the consumer gives up.

#### `for await` Over an Array of Promises

`for await` also works on an ordinary array that is not an asynchronous iterator: it
awaits each element with `await`. This is the short way to consume an array of
promises in order.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
};

function requestMeasurement(station) {
  return new Promise((resolve) => {
    const record = SOURCE[station];
    setTimeout(() => resolve({ station, value: record.value }), record.delay);
  });
}

const promises = ["A1", "B2", "C3"].map((station) => requestMeasurement(station));
for await (const measurement of promises) {
  console.log("consumed in order:", measurement.station, measurement.value);
}
```

```
consumed in order: A1 21.4
consumed in order: B2 19.8
consumed in order: C3 23.1
```

The distinction here matters: the requests started **together**, the results were
consumed **in order**. Results are processed in the array's order; A1 is awaited
first even though it is the slowest. This is the desired behavior when processing
order matters.

#### Backpressure

The concept of **backpressure**, introduced when queues were covered in the Data
Structures course, sits at the center of asynchronous streams: if the producer is
faster than the consumer, the gap between them has to pile up **somewhere**.

In a **push-based** source, the producer does not wait for the consumer; items pile
up in a buffer.

```js
const buffer = [];
let produced = 0;

const id = setInterval(() => {
  produced += 1;
  buffer.push({ order: produced });
  if (produced === 6) clearInterval(id);
}, 5);

await new Promise((resolve) => setTimeout(resolve, 45));
console.log("produced:", produced, "— waiting in buffer:", buffer.length);
```

```
produced: 6 — waiting in buffer: 6
```

Since the consumer never reads, all six of the six items sit waiting in memory. Had
production been open-ended, the buffer would have grown without limit and exhausted
memory. This is one of the leak forms covered in the memory topic.

In a pull-based stream, this problem does not exist by definition: the generator
produces the next value only when it is requested, so there is nothing to pile up.
Putting a push-based source behind a pull-based interface — that is, bounding the
buffer and stopping production once it fills — is the basic pattern of stream
design.

#### Summary

- The asynchronous iterator protocol is identical to the ordinary iteration
  protocol, except that `next` returns its result as a promise; the object defines
  `Symbol.asyncIterator`.
- Generators in `async function*` form take over the protocol's state management
  and allow both `await` and `yield` in the body.
- `for await` waits for the next value on every turn; a rejection in the stream is
  thrown at the loop's location and terminates the loop.
- On early exit, the generator's `finally` block runs; cleanup for continuous
  resources is written there.
- In a pull-based stream, the consumer sets the pace and backpressure forms on its
  own; in a push-based stream, memory runs out if the buffer is not bounded.

#### Next Step

The measurement source used in this topic was always simulated with a timer, in the
same process. The next topic moves the source **outside the process**: requests and
responses over the network. There, the structure of request and response objects,
reading a body as a stream, and this lesson's asynchronous iteration applied
directly to network bodies will be taken up.

## Network and Data Access

Request and response objects, streaming bodies, controlling request lifetime with an abort signal, timeouts, and retry patterns for transient errors.

### HTTP Requests

Source: https://academia.sh/en/courses/asynchronous-javascript/http-requests

The structure of request and response objects, why a status code is not treated as an error, the body being readable only once, and processing a streaming body chunk by chunk with asynchronous iteration.

Up to this point, the measurement source was always mimicked with a timer in the same
process. A real source lives outside the process: the request travels over the network,
the response comes back in pieces, and there is an uncontrollable delay in between.

This lesson moves the source outside the process. The request–response structure
covered at the protocol level in the How the Internet Works course is taken up here in
its JavaScript form; the measurement stream also becomes readable from an HTTP service.

The server used in the examples is started at a loopback address inside the same
program. Port zero is given; the host environment picks a free port, and the chosen
value is variable, so it is never printed anywhere.

#### Request and Response Are Objects

A request and a response are represented by two objects independent of the details
sitting below the network layer. Both can also be created without a network, which
makes examining their structure easier.

```js
const request = new Request("http://example.invalid/measurement/B2", {
  method: "POST",
  headers: { "content-type": "application/json", "x-client": "measurement-collector" },
  body: JSON.stringify({ station: "B2", value: 19.8 }),
});

console.log("method:", request.method);
console.log("path:", new URL(request.url).pathname);
console.log("header:", request.headers.get("x-client"));
console.log("body:", await request.json());

const response = new Response(JSON.stringify({ state: "recorded" }), {
  status: 201,
  headers: { "content-type": "application/json" },
});

console.log("response status:", response.status, response.ok);
console.log("response body:", await response.json());
```

```
method: POST
path: /measurement/B2
header: measurement-collector
body: { station: 'B2', value: 19.8 }
response status: 201 true
response body: { state: 'recorded' }
```

Three things stand out. Headers are held not in a plain object but in a separate
structure offering name–value access. The methods that read the body (`json`, `text`)
return a promise, because the body may not have fully arrived yet. The status code and
the `ok` field travel together: `ok` states whether the status code falls in the
success range.

#### A Network Error and a Failed Status Code Are Separate

The function that sends a request rejects only when it **cannot deliver** the
request: a connection could not be established, a name could not be resolved, the
connection dropped. If the server returned a response — whatever the status code — the
returned promise fulfills.

```js
import { createServer } from "node:http";

const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
};

const server = createServer((request, response) => {
  const path = new URL(request.url, "http://local").pathname;
  const station = path.replace("/measurement/", "");
  const record = SOURCE[station];

  if (record === undefined) {
    response.writeHead(404, { "content-type": "application/json" });
    response.end(JSON.stringify({ error: "unknown station", station }));
    return;
  }

  setTimeout(() => {
    response.writeHead(200, { "content-type": "application/json" });
    response.end(JSON.stringify({ station, value: record.value }));
  }, record.delay);
});

await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const base = `http://127.0.0.1:${server.address().port}`;

const response = await fetch(`${base}/measurement/B2`);
console.log("status:", response.status, "ok:", response.ok);
console.log("content type:", response.headers.get("content-type"));
console.log("body:", await response.json());

const notFound = await fetch(`${base}/measurement/Z9`);
console.log("status:", notFound.status, "ok:", notFound.ok);
console.log("body:", await notFound.json());

server.close();
```

```
status: 200 ok: true
content type: application/json
body: { station: 'B2', value: 19.8 }
status: 404 ok: false
body: { error: 'unknown station', station: 'Z9' }
```

The unknown-station request did not **throw** an error; it returned a valid response
with a 404 status. This is where a common mistake comes from: a `try`/`catch` block
alone does not catch failed requests.

The correct pattern is to explicitly check the status code and turn a failed response
into an error. Once the check is pulled into its own function, it does not need to be
repeated at every call site:

```js
function check(response) {
  if (!response.ok) throw new Error(`request failed: ${response.status}`);
  return response;
}

const success = new Response("data", { status: 200 });
const failure = new Response("not found", { status: 404 });

console.log("successful response passed:", check(success).status);

try {
  check(failure);
} catch (error) {
  console.log("check caught:", error.message);
}
```

```
successful response passed: 200
check caught: request failed: 404
```

The distinction is meaningful: a network error means "it is not known whether the
request reached the server"; a failed status code means "it reached, the server
evaluated it, and rejected it." The two call for different recovery strategies — this
distinction becomes decisive in the retry lesson.

#### The Body Is Read Once

A response body is a stream; it is exhausted once read. A second read attempt produces
an error.

```js
const response = new Response(JSON.stringify({ station: "B2", value: 19.8 }), {
  headers: { "content-type": "application/json" },
});

console.log("body used:", response.bodyUsed);
console.log("first read:", await response.json());
console.log("body used:", response.bodyUsed);

try {
  await response.json();
} catch (error) {
  console.log("second read error:", error.constructor.name);
}
```

```
body used: false
first read: { station: 'B2', value: 19.8 }
body used: true
second read error: TypeError
```

This limit is a direct consequence of the body not being held in memory. If the same
body needs to be looked at twice, there are two paths: storing the result in a
variable, or duplicating the response with `clone`. Duplicating means two readers each
consume the same data separately, and it keeps a copy in memory; this is why storing
the result is the default choice.

#### Processing the Body as a Stream

Waiting for the entire body costs both delay and memory on large responses. The body is
an asynchronous iterator: the previous lesson's `for await` loop applies directly.

```js
import { createServer } from "node:http";

const server = createServer((request, response) => {
  response.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
  const lines = ["A1;21.4", "B2;19.8", "C3;23.1"];
  let index = 0;

  function nextLine() {
    if (index === lines.length) {
      response.end();
      return;
    }
    response.write(`${lines[index]}\n`);
    index += 1;
    setTimeout(nextLine, 20);
  }
  nextLine();
});

await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const base = `http://127.0.0.1:${server.address().port}`;

const response = await fetch(`${base}/stream`);
const decoder = new TextDecoder();
let chunkCount = 0;

for await (const chunk of response.body) {
  chunkCount += 1;
  console.log("chunk", chunkCount, "—", decoder.decode(chunk, { stream: true }).trim());
}
console.log("total chunks:", chunkCount);

server.close();
```

```
chunk 1 — A1;21.4
chunk 2 — B2;19.8
chunk 3 — C3;23.1
total chunks: 3
```

The server wrote the three lines twenty milliseconds apart; the client processed each
line the moment it arrived, without waiting for the last one. The measurement stream
thereby became a stream in the real sense: processing can begin before the data is
complete.

Two details matter for portability. First, chunk boundaries are **not the same as data
boundaries**; the network can split or merge lines. In this example, chunks lined up
with lines because a delay was placed between each write, but in the general case they
do not line up. The correct implementation is a parser that appends incoming chunks to
a buffer and searches for line endings.

Second, the `stream` option given to the decoder correctly handles the case where a
multi-byte character is split across two chunks. This is the direct practical
counterpart of the character encoding topic from the How Computers Work course: a byte
boundary and a character boundary are not the same thing.

#### The Remaining Gap

An assumption was hidden in this lesson's programs: the server responds sooner or
later. If it does not, `await` waits forever; neither a result nor an error ever comes.
This gap was pointed out in the promise-concept lesson and is still open.

In the same way, there is no way to give up on the result of a request that has been
started: as seen in the combinators lesson, not awaiting something is not the same as
canceling it. The request continues, the response arrives, memory is held.

#### Summary

- Request and response are objects; headers are held in a separate structure offering
  name–value access, and body-reading methods return a promise.
- The function that sends a request rejects only on a delivery failure; a failed status
  code is a valid response and should be checked through the `ok` field.
- The body is a stream and is read once; a second read throws an error.
- The body is an asynchronous iterator and can be processed chunk by chunk with
  `for await`.
- Chunk boundaries do not line up with data boundaries; parsing has to be done with a
  buffer.

#### Next Step

The next lesson closes the two gaps left open together: putting a time limit on a
request, and actually stopping a request that has already started. The abort signal
concept will be introduced, the timeout will be built on top of that signal, and it
will be shown why a race-based solution that only cuts the waiting falls short.

### Cancellation and Timeouts

Source: https://academia.sh/en/courses/asynchronous-javascript/cancellation-and-timeouts

The abort signal concept, actually stopping a request, building a timeout on top of the signal, why a race-based timeout falls short, and combining signals.

The previous lesson closed with two gaps: a source that never responds is waited on
forever, and there is no way to give up on a request that has already started. Both
gaps come from the same absence — there is no channel to affect an asynchronous
operation *from outside*.

This lesson builds that channel. Its name is the **abort signal**; a timeout is a
special case built on top of it. The measurement stream becomes stoppable here.

#### The Abort Signal

The cancellation mechanism consists of two objects. The **controller** sits on the side
that starts the cancellation and carries an `abort` method. The **signal** is handed to
the side doing the work; it reports whether cancellation has happened and emits an
event at the moment of cancellation.

The distinction matters: the side receiving the signal *cannot* cancel, it can only
*watch* for cancellation. This is how the authority to cancel stays with the caller.

```js
import { createServer } from "node:http";

const server = createServer((request, response) => {
  const id = setTimeout(() => {
    response.writeHead(200, { "content-type": "application/json" });
    response.end(JSON.stringify({ station: "A1", value: 21.4 }));
  }, 200);
  response.on("close", () => clearTimeout(id));
});

await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const base = `http://127.0.0.1:${server.address().port}`;

const controller = new AbortController();
setTimeout(() => controller.abort(), 20);

try {
  await fetch(`${base}/measurement/A1`, { signal: controller.signal });
  console.log("this line never runs");
} catch (error) {
  console.log("request stopped:", error.name);
}

try {
  await fetch(`${base}/measurement/A1`, { signal: AbortSignal.timeout(20) });
} catch (error) {
  console.log("timeout:", error.name);
}

server.close();
```

```
request stopped: AbortError
timeout: TimeoutError
```

The server was set up to respond after two hundred milliseconds; both requests ended at
the twentieth millisecond. The error names tell the two cases apart: a manual abort
produces `AbortError`, a time limit produces `TimeoutError`. This distinction is used in
the recovery decision — if the user gave up, retrying is meaningless; if the time
ran out, it can be.

`AbortSignal.timeout` is a shortcut: it produces a signal that aborts on its own once
the given duration elapses. It removes the need to set up and cancel a timer by hand.

The `close` listener on the server side is the counterpart of the same idea: when the
client cuts the connection, the server also cancels its own pending work. Cancellation
is not a one-way call, it is a contract that has to be implemented on both ends.

#### Adding Cancellation to Your Own Function

Cancellation is not the host environment's privilege. Any function returning a promise
can join the same contract by accepting a signal in its options object. There are
three rules: reject immediately if the signal is already aborted, release resources and
reject on the abort event, and register the listener to run only once.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
};

function fetchMeasurement(station, options = {}) {
  const signal = options.signal;
  return new Promise((resolve, reject) => {
    if (signal?.aborted) {
      reject(signal.reason);
      return;
    }
    const record = SOURCE[station];
    const id = setTimeout(() => {
      console.log("source: work complete —", station);
      resolve({ station, value: record.value });
    }, record.delay);

    signal?.addEventListener(
      "abort",
      () => {
        clearTimeout(id);
        console.log("source: work stopped —", station);
        reject(signal.reason);
      },
      { once: true },
    );
  });
}

const controller = new AbortController();
setTimeout(() => controller.abort(new Error("collection cancelled")), 15);

try {
  await fetchMeasurement("A1", { signal: controller.signal });
} catch (error) {
  console.log("caller saw:", error.message);
}

await new Promise((resolve) => setTimeout(resolve, 40));
console.log("wait finished; no further output for A1");
```

```
source: work stopped — A1
caller saw: collection cancelled
wait finished; no further output for A1
```

The last line is proof that the cancellation really did stop the work: even though
forty more milliseconds were waited, the "work complete" message never printed, because
the timer was cleared.

The value given to the `abort` method sits in the signal's `reason` field and becomes
the rejection reason. If none is given, the host environment produces an error named
`AbortError`. Giving your own error is the most direct way to carry the cancellation
reason without a call stack.

#### Why a Race-Based Timeout Falls Short

A timeout can also be written the first way that comes to mind once combinators are
known: racing the work against a timer. This fix cuts the waiting, but it does not cut
the work.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
};

function fetchMeasurement(station) {
  return new Promise((resolve) => {
    const record = SOURCE[station];
    setTimeout(() => {
      console.log("source: work completed anyway —", station);
      resolve({ station, value: record.value });
    }, record.delay);
  });
}

function timeoutAfter(duration) {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error(`timeout: ${duration} ms`)), duration);
  });
}

try {
  await Promise.race([fetchMeasurement("A1"), timeoutAfter(10)]);
} catch (error) {
  console.log("caller saw:", error.message);
}

await new Promise((resolve) => setTimeout(resolve, 40));
console.log("wait finished");
```

```
caller saw: timeout: 10 ms
source: work completed anyway — A1
wait finished
```

The caller got the error at the tenth millisecond; the work completed at the thirtieth.
That is, the request sent to the server continued, the response arrived, memory and the
connection were held — only no one used the result.

The consequences fall into three headings: the source is spent for nothing, the
unsettled promise and the values it closes over stay in memory, and in an operation
with a side effect — writing a record, say — the operation said to have "timed out" may
actually have completed.

The rule is this: **a race ends the waiting, cancellation ends the work.** The two are
not interchangeable; a correct timeout is one that triggers cancellation once the
duration elapses.

#### Combining Signals

An operation can have more than one reason to cancel: the user gave up, the time ran
out, a higher-level operation was cancelled. `AbortSignal.any` gathers multiple signals
into a single signal; whichever aborts first, the combined signal aborts with that
one's reason.

```js
const user = new AbortController();
const combined = AbortSignal.any([user.signal, AbortSignal.timeout(30)]);

combined.addEventListener("abort", () => {
  console.log("combined signal stopped; reason:", combined.reason.name);
});

setTimeout(() => user.abort(), 10);

await new Promise((resolve) => setTimeout(resolve, 50));
console.log("user signal aborted:", user.signal.aborted);
```

```
combined signal stopped; reason: AbortError
user signal aborted: true
```

Because the user's cancellation arrived at the tenth millisecond, the combined signal
settled with that one; the thirty-millisecond time limit never came into play. This
pattern makes it possible to carry cancellation through a call tree: a top-level
operation's signal is passed down, combined with each sub-operation's own time limits.

#### The Limits of Cancellation

Two limits need to be known explicitly.

Cancellation **relies on cooperation.** A function that does not watch the signal
cannot be cancelled; the cancellation request only reaches those listening for it.
This is why every function returning a promise should accept a signal parameter for
work that might run long.

Cancellation **is not undoing.** A request's effect once it has reached the server is
not undone by cancellation; only waiting for the response is given up on. In operations
with side effects, this distinction is the basis of the next lesson's retry decision.

#### Summary

- Cancellation is set up with two objects, a controller and a signal; the side
  receiving the signal only watches for cancellation.
- Your own functions returning a promise can join the same contract by accepting a
  signal and releasing their resources on the abort event.
- `AbortSignal.timeout` produces a time limit as a signal; a manual abort gives
  `AbortError`, a time limit gives `TimeoutError`.
- A race-based timeout only cuts the waiting; the work continues, the resource is held,
  a side effect can still occur.
- `AbortSignal.any` gathers multiple cancellation reasons into one signal and lets
  cancellation be carried through a call tree.

#### Next Step

Cancellation and timeouts made failure well-defined: it is now known when a request
will end. What remains is what to do after a failure. The next lesson separates
transient errors from permanent ones, determines which operations can safely be
retried, shows why wait times are chosen with growth, and why a retry budget is kept
limited. The measurement stream thereby becomes both stoppable and resilient.

### Error and Retry Patterns

Source: https://academia.sh/en/courses/asynchronous-javascript/error-and-retry-patterns

Separating transient errors from permanent ones, retrying with growing wait times, the retry budget, idempotence through an operation key, and preventing mass failure.

Cancellation and timeouts made failure well-defined: it is now known when a request
will end. What remains is what to do after a failure.

Not every failure is the same. Some pass on their own — a brief overload, the loss of a
single packet. Some give the same result when repeated — a wrong address, an unknown
station. This lesson builds that distinction, writes the correct form of retrying, and
determines which operations can be repeated safely.

#### Transient and Permanent Errors

There is exactly one reason to retry: **the chance that the same request succeeds this
time.** If that chance does not exist, retrying both keeps the caller waiting and keeps
the server busy for nothing.

| Error | Class | Reason |
|---|---|---|
| Connection could not be established | Transient | Network conditions can change |
| 408, 429 | Transient | Time or rate limit; can be tried later |
| 500, 502, 503, 504 | Transient | Temporary state on the server side |
| 400, 401, 403, 404 | Permanent | The same result even if the request is resent |

The distinction is not formal, it is semantic: 404 says "this resource does not
exist"; asking again does not change the answer. 503 says "I cannot serve right now";
this is a time-dependent statement.

The distinction between a network error and a failed status code is decisive here too.
As shown in the previous lesson, a network error is the case where it is not known
whether the request reached the server. This uncertainty makes retrying dangerous for
side-effecting operations.

#### Retrying With Growing Wait

Repeating a failed request immediately makes the problem worse: an overloaded server
faces every client that gave up retrying at once. This is why the wait time is
increased on every attempt — **exponential backoff**.

```js
import { createServer } from "node:http";

let requestCount = 0;

const server = createServer((request, response) => {
  requestCount += 1;
  if (requestCount <= 2) {
    response.writeHead(503, { "content-type": "application/json" });
    response.end(JSON.stringify({ error: "service temporarily unavailable" }));
    return;
  }
  response.writeHead(200, { "content-type": "application/json" });
  response.end(JSON.stringify({ station: "A1", value: 21.4 }));
});

await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const base = `http://127.0.0.1:${server.address().port}`;

const TRANSIENT_STATUSES = new Set([408, 429, 500, 502, 503, 504]);

function wait(duration) {
  return new Promise((resolve) => setTimeout(resolve, duration));
}

async function fetchWithRetry(url, attempts, baseDelay) {
  let lastError = null;

  for (let attempt = 1; attempt <= attempts; attempt += 1) {
    const response = await fetch(url);
    if (response.ok) {
      console.log("attempt", attempt, "— successful");
      return await response.json();
    }
    if (!TRANSIENT_STATUSES.has(response.status)) {
      throw new Error(`permanent error: ${response.status}`);
    }
    lastError = new Error(`transient error: ${response.status}`);
    console.log("attempt", attempt, "— transient error:", response.status);

    if (attempt < attempts) {
      const delay = baseDelay * 2 ** (attempt - 1);
      console.log("  planned wait:", delay, "ms");
      await wait(delay);
    }
  }

  throw new Error(`retry budget exhausted: ${attempts}`, { cause: lastError });
}

console.log("result:", await fetchWithRetry(`${base}/measurement/A1`, 4, 10));

server.close();
```

```
attempt 1 — transient error: 503
  planned wait: 10 ms
attempt 2 — transient error: 503
  planned wait: 20 ms
attempt 3 — successful
result: { station: 'A1', value: 21.4 }
```

The server was set up to turn away the first two requests; the third attempt
succeeded. The printed wait values are not a measurement, they are the computed plan:
the base value is multiplied by two on every attempt.

In real deployments, a random offset is also added to this plan. The reason is this: if
clients that fail at the same moment all apply the same plan, their retries also land
on the same moment, and the server faces a second wave. The offset spreads the wave
across time. It was not used in this lesson so that the outputs stay deterministic.

#### Retry Budget and Early Exit on a Permanent Error

Two safeguards are required. The number of attempts has to be bounded, or the caller
waits forever when the service never returns. On a permanent error, there should be no
wait at all.

```js
import { createServer } from "node:http";

const server = createServer((request, response) => {
  const path = new URL(request.url, "http://local").pathname;
  if (path === "/measurement/Z9") {
    response.writeHead(404, { "content-type": "application/json" });
    response.end(JSON.stringify({ error: "unknown station" }));
    return;
  }
  response.writeHead(503, { "content-type": "application/json" });
  response.end(JSON.stringify({ error: "service temporarily unavailable" }));
});

await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const base = `http://127.0.0.1:${server.address().port}`;

const TRANSIENT_STATUSES = new Set([408, 429, 500, 502, 503, 504]);

function wait(duration) {
  return new Promise((resolve) => setTimeout(resolve, duration));
}

async function fetchWithRetry(url, attempts, baseDelay) {
  let lastError = null;

  for (let attempt = 1; attempt <= attempts; attempt += 1) {
    const response = await fetch(url);
    if (response.ok) return await response.json();
    if (!TRANSIENT_STATUSES.has(response.status)) {
      throw new Error(`permanent error: ${response.status}`);
    }
    lastError = new Error(`transient error: ${response.status}`);
    console.log("attempt", attempt, "— transient error:", response.status);
    if (attempt < attempts) await wait(baseDelay * 2 ** (attempt - 1));
  }

  throw new Error(`retry budget exhausted: ${attempts}`, { cause: lastError });
}

try {
  await fetchWithRetry(`${base}/measurement/Z9`, 4, 10);
} catch (error) {
  console.log("no wait on permanent error:", error.message);
}

try {
  await fetchWithRetry(`${base}/measurement/A1`, 3, 10);
} catch (error) {
  console.log("final error:", error.message);
  console.log("root cause:", error.cause.message);
}

server.close();
```

```
no wait on permanent error: permanent error: 404
attempt 1 — transient error: 503
attempt 2 — transient error: 503
attempt 3 — transient error: 503
final error: retry budget exhausted: 3
root cause: transient error: 503
```

A single request was made for the unknown station; three attempts were spent on the
address giving a transient error, and once the budget ran out, the error was handed
upward carrying its root cause. The `cause` pattern from the error-handling lesson is
needed here for diagnosis: the caller sees both "how many attempts before giving up"
and "what happened most recently."

The total duration of retrying is also a limit. Once the number of attempts and the
wait plan are combined, the worst-case elapsed time should be computable in advance;
the abort signal can also be bound to this total duration.

#### Retrying Side-Effecting Operations

Reading a measurement is harmless: sending the same request twice just does extra
work. **Writing** a measurement is not. When you get a network error, you cannot know
whether the request reached the server; if you resend it, the same record can be
created twice.

The fix is adding an **operation key** to the request that makes it unique. The server
does not count a second request arriving with the same key as a new operation; it
returns the first operation's result.

```js
import { createServer } from "node:http";

const records = [];

const server = createServer((request, response) => {
  let body = "";
  request.on("data", (chunk) => {
    body += chunk;
  });
  request.on("end", () => {
    const key = request.headers["x-operation-key"];
    const existing = key === undefined ? undefined : records.find((r) => r.key === key);

    if (existing === undefined) {
      records.push({ key, data: JSON.parse(body) });
    }
    response.writeHead(200, { "content-type": "application/json" });
    response.end(JSON.stringify({ totalRecords: records.length }));
  });
});

await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const base = `http://127.0.0.1:${server.address().port}`;

async function writeMeasurement(headers) {
  const response = await fetch(`${base}/record`, {
    method: "POST",
    headers: { "content-type": "application/json", ...headers },
    body: JSON.stringify({ station: "A1", value: 21.4 }),
  });
  return await response.json();
}

console.log("no-key attempt 1:", await writeMeasurement({}));
console.log("no-key attempt 2:", await writeMeasurement({}));
console.log("keyed attempt 1:", await writeMeasurement({ "x-operation-key": "write-77" }));
console.log("keyed attempt 2:", await writeMeasurement({ "x-operation-key": "write-77" }));

server.close();
```

```
no-key attempt 1: { totalRecords: 1 }
no-key attempt 2: { totalRecords: 2 }
keyed attempt 1: { totalRecords: 3 }
keyed attempt 2: { totalRecords: 3 }
```

The two requests without a key produced two records; the two requests with a key
produced a single record. The property that repeating the same request does not
change the result is called **idempotence**; the How the Internet Works course
introduced the same property for HTTP methods. The addition here is that the property
can be provided through a key in cases where it does not come from the method.

The rule is summarized as: **retrying is safe only if the operation is idempotent.** If
it is not, it has to be made idempotent first, and only then retried.

#### The Limit of Retrying

Retrying rescues a single request; it does not rescue the system. If the service has
genuinely gone down, every client continuing to retry adds to the load and delays
recovery.

For this reason, applications bound retrying with a higher-level mechanism: once
consecutive failures cross a certain threshold, requests are not sent at all for a
while, then the state is tested with a single probe request. Another form of the same
idea is capping the total number of attempts running at the same time.

For the measurement stream, the result is this: a few attempts are made per station,
a permanent error gives up immediately, once the budget runs out the station is marked
"no data," and collection continues. Resilient aggregation built with `allSettled` is
the natural home for this behavior.

#### Summary

- Retrying is meaningful only on transient errors; on a permanent error it both keeps
  the caller waiting and keeps the server busy for nothing.
- The wait time is increased on every attempt; fixed-interval retries hit an
  overloaded service as a second wave.
- The number of attempts and the total duration should be bounded, and once the budget
  runs out, the error should be handed upward together with its root cause.
- A side-effecting operation can be retried only if it is idempotent; idempotence is
  provided through an operation key.
- Retrying rescues a single request; a system-level bounding mechanism is also needed.

#### Next Step

The network topic is complete: a request was made, bounded, cancelled, and made
resilient. These lessons said "stays in memory" a few times without explaining what,
why, and for how long. The next topic covers this: the memory allocation and release
cycle, reachability-based collection, what asynchronous code holds in memory, and
diagnosing leaks with tools.

## Memory and Performance

The allocation and release cycle of memory, reachability-based garbage collection, leak diagnosis by snapshot comparison, main-thread profiling, and debugging tools.

### Memory Lifecycle

Source: https://academia.sh/en/courses/asynchronous-javascript/memory-lifecycle

The phases of allocation, use, and release; reachability's root set; what timers, listeners, and unsettled promises hold onto in memory in asynchronous code.

The previous topic said "stays in memory" a few times: promises that lost a race,
requests that were not canceled, buffers that were never consumed. What stays, why, and
for how long was not explained.

This topic explains it. The starting point is the three-phase cycle common to every
programming language; what sets JavaScript apart is who owns the third phase.

#### Three Phases

**Allocation.** Room is set aside for a value. In JavaScript this is never requested
explicitly; it happens on its own when an object, array, string, or function is written.

**Use.** The allocated room is read and written. This is the program's actual work.

**Release.** The room is given back. In languages like C, the program does this; in
JavaScript, the runtime does.

The third phase being automatic does not mean the problem disappears; it transforms the
problem. In manually managed memory, the error is "releasing too early" — the dangling
pointer from the How Computers Work course. In automatic management, the error is
"holding a reference longer than necessary." The first crashes the program, the second
bloats it.

Objects live in the **heap** region of the memory layout introduced in the same course;
local variables are kept in a frame on the call stack. The frame disappears when the
function returns; an object in the heap becomes collectible once no one refers to it
anymore.

#### Reachability and Roots

The runtime cannot know whether an object is "needed." Instead, it uses a measurable
criterion: **reachability**. Every object reachable by starting from a root set and
following references stays alive; every object unreachable is collectible.

The root set consists of:

- Local variables and parameters on the running call stack
- Bindings in the global scope
- Module-level bindings
- **Callbacks of pending timers**
- **Registered event listeners**
- **Callbacks of unsettled promises**

The last three are this course's subject and the source of most memory problems. When a
timer is set up, its callback and every variable that callback closes over become a root.
The closure concept was defined in the Objects and Functions in JavaScript course; the
addition here is that a closure's **lifetime is determined by an asynchronous event**.

#### When an Object Becomes Collectible

The rule can be shown with a runnable example. The program below produces two objects:
one used only inside a function, the other closed over by a timer callback.

Two tools are used to observe the moment of collection. `FinalizationRegistry` runs a
callback when a registered object is collected. To manually trigger collection, the
runtime is also started with the relevant option; otherwise the collector runs on its own
schedule and the output is not deterministic.

```js
const registry = new FinalizationRegistry((label) => {
  console.log("collected:", label);
});

function transientMeasurement() {
  const reading = { station: "A1", value: 21.4 };
  registry.register(reading, "transient measurement");
  return reading.value;
}

function heldByTimer() {
  const reading = { station: "B2", value: 19.8 };
  registry.register(reading, "measurement held by timer");
  return setTimeout(() => console.log("timer ran:", reading.station), 60);
}

console.log("transient measurement's value:", transientMeasurement());
const id = heldByTimer();

global.gc();
await new Promise((resolve) => setTimeout(resolve, 10));
global.gc();
console.log("first collection round finished");

clearTimeout(id);
global.gc();
await new Promise((resolve) => setTimeout(resolve, 10));
global.gc();
await new Promise((resolve) => setTimeout(resolve, 10));
console.log("round after timer canceled finished");
```

```
$ node --expose-gc memory.mjs
transient measurement's value: 21.4
collected: transient measurement
first collection round finished
collected: measurement held by timer
round after timer canceled finished
```

The result shows two rules at once. The first object became unreachable the moment the
function returned and was collected in the first round — what was returned was only a
number, not the object itself. The second object could not be collected as long as the
timer was pending; it was collected only after `clearTimeout` removed the root.

The `--expose-gc` option here is for observation only. Manually calling the collector in
application code is neither required nor correct; the decision belongs to the runtime.

#### Asynchronous Roots and the Measurement Stream

The same rule applies to every construct in the course's running example.

**A self-scheduling stream.** The `startStream` function in the Timers topic sets up a
new timer on every round. As long as the stream is not stopped, the callback and the
counters, buffer, and station list it closes over stay in memory. The function that stops
the stream is at the same time a memory function.

**An uncanceled request.** In a race-based timeout, the losing promise never settles;
because an unsettled promise's callbacks are also a root, the buffer and response body
the request holds cannot be released.

**A listener that is not removed.** In the Abort Signal lesson, the listener was
registered with the `once` option. Without that option, the listener would stay bound to
the signal object, which would in turn hold onto everything the callback closes over.

The shared lesson of the three examples is this: **starting an asynchronous job is at
the same time creating a root.** The code that finishes or cancels the job is the code
that removes that root.

#### What a Closure Holds

It is assumed that a closure holds *every* value in its own scope. Runtimes are not
required to behave this way: only the bindings the inner function actually references are
kept; the rest are treated like ordinary local variables.

```js
const collected = [];
const registry = new FinalizationRegistry((label) => {
  collected.push(label);
});

function setUpClosure() {
  const rawBuffer = { name: "raw measurement array" };
  const summary = { station: "A1", value: 21.4 };
  registry.register(rawBuffer, "raw buffer");
  registry.register(summary, "summary");

  return function giveSummary() {
    return summary.value;
  };
}

const giveSummary = setUpClosure();

global.gc();
await new Promise((resolve) => setTimeout(resolve, 10));
global.gc();
await new Promise((resolve) => setTimeout(resolve, 10));

console.log("value the closure gives:", giveSummary());
console.log("collected:", collected.sort().join(", ") || "(none)");
console.log("measurement fields:", Object.keys(process.memoryUsage()).sort().join(", "));
```

```
$ node --expose-gc closure.mjs
value the closure gives: 21.4
collected: raw buffer
measurement fields: arrayBuffers, external, heapTotal, heapUsed, rss
```

Both objects were locals of the same function; the returned closure referenced only one
of them. The unreferenced buffer was collected, the referenced summary stayed as long as
the closure lived.

Even so, this behavior is not a guarantee; which bindings a closure keeps is the
runtime's decision. The portable rule is: **in a closure that will live long, close over
only what is necessary.** If only a single field is needed from a large structure,
building the closure around that field instead of the whole structure is both clearer and
safer.

The last line shows which axes memory usage is reported on. Because the values change by
machine and load, only the field names are printed here: `rss` is the total memory the
process gets from the operating system, `heapTotal` is the allocated heap size,
`heapUsed` is the portion actually used in the heap, and `external` and `arrayBuffers`
are binary data held outside the JavaScript heap. The field usually watched in leak
tracking is `heapUsed`.

#### When Release Becomes Visible

An object becoming unreachable and memory actually being given back are not the same
moment. The collector runs on its own schedule; measured memory usage may not drop
immediately after unreachability.

This has two practical consequences. First, the observation "memory did not drop" is not
proof of a leak on its own; the measurement has to be repeated across more than one
collection round. Second, when a `FinalizationRegistry` callback runs — or even whether it
runs at all — is not guaranteed; it may never run while the program is exiting. For this
reason, resource cleanup is not written there; cleanup is done with an explicit close call
or a `finally` block.

#### Summary

- Memory's lifecycle consists of allocation, use, and release phases; in JavaScript the
  runtime does the third.
- Automatic management eliminates the dangling-pointer error and replaces it with the
  "holding a reference longer than necessary" error.
- An object becomes collectible once it is unreachable from the root set; pending timers,
  registered listeners, and unsettled promises are also among the roots.
- Starting an asynchronous job creates a root; cancellation or completion removes that
  root.
- A closure can hold only the bindings it references; not closing over unnecessary values
  in long-lived closures is the portable rule.
- Unreachability and memory being given back are not the same moment; finalization
  callbacks cannot be used for cleanup.

#### Next Step

In this lesson, the collector was used as a black box: it was said to "collect the
unreachable" and was triggered by hand. The next lesson opens the box. How reachability
is computed, why the reference-counting method fails on cycles, how the generational
hypothesis speeds up the collector, and what gap weak-reference structures fill in this
scheme will be covered.

### Garbage Collection

Source: https://academia.sh/en/courses/asynchronous-javascript/garbage-collection

How reachability-based collection works, why reference counting fails on cycles, the generational hypothesis, incremental collection's pauses, and weak-reference structures.

The previous lesson used the collector as a black box: it was said to "collect the
unreachable." This lesson opens the box. The goal is not to write a collector, but to be
able to predict its behavior — specifically, where pauses come from and what gap weak
references fill.

#### Mark and Sweep

The base method has two phases. In the **mark** phase, starting from the root set, every
object reached by following references is marked. In the **sweep** phase, the room of
unmarked objects is reclaimed.

This is a direct application of the graph traversal from the Data Structures course:
objects are nodes, references are edges; marking is a reachability traversal starting from
the roots. Visit-marking does the same job here too — an already-seen object is not
traversed again.

The method's decisive property is this: its cost depends not on the number of dead
objects, but on the **number of live objects** — dead objects are never marked. Sweeping,
however, walks the whole heap; that is where the room held by dead objects is reclaimed.

#### Why Reference Counting Is Not Enough

An alternative method is for every object to count how many references point to it. When
the count drops to zero, the object is released. This method is fast and produces no
pause; but there is one class it can never collect at all: objects referencing each
other.

```js
const collected = [];
const registry = new FinalizationRegistry((label) => {
  collected.push(label);
});

function setUpCycle() {
  const a = { name: "node A1" };
  const b = { name: "node B2" };
  a.neighbor = b;
  b.neighbor = a;
  registry.register(a, "A1 in the cycle");
  registry.register(b, "B2 in the cycle");
  console.log("cycle set up:", a.neighbor.name, "<->", b.neighbor.name);
}

setUpCycle();

global.gc();
await new Promise((resolve) => setTimeout(resolve, 10));
global.gc();
await new Promise((resolve) => setTimeout(resolve, 10));
console.log("collected:", collected.sort().join(", "));
```

```
$ node --expose-gc cycle.mjs
cycle set up: node B2 <-> node A1
collected: A1 in the cycle, B2 in the cycle
```

Because collection order is unpredictable, the names were accumulated and printed sorted;
this is an early example of the next lesson's warning — **which** object the collector
collects can be observed, **in what order** it collects cannot be relied on for
observation.

Each of the two objects had one reference pointing to it, and those references were
never zeroed out; even so, both were collected. The reason is clear: neither was reachable
from the root set. The reachability criterion does not ask "how many references are
there," it asks "is it reached from the roots," and cycles answer this question
correctly.

The distinction matters in practice for a structure encountered often: two objects
referencing each other — a parent and child item, an observer and the observed, a
doubly-linked-list node — are not a source of leaks in JavaScript. A leak comes from a
root, not from a cycle.

#### The Generational Hypothesis

Measurements give a consistent observation: **most objects live briefly.** Intermediate
objects produced inside a function die immediately; structures that live for the whole
application are few.

Collectors use this observation. The heap is split into at least two regions: a young
region where new objects are allocated, and an old region where objects that survive
collection are moved. The young region is scanned often and quickly; the old region is
scanned rarely.

The consequence is that a commonly performed micro-optimization is unnecessary: avoiding
producing short-lived objects brings no gain in most cases, because the collector is
already tuned for exactly this situation. The real cost is objects that move to the old
region and stay there unnecessarily.

#### Pauses

The object graph must not change during marking; otherwise marking becomes inconsistent.
For this reason the program stops while the collector runs — in a single-threaded
environment, this means the event loop stops too.

Two techniques are used to shrink the pause. **Incremental collection** splits marking
into small pieces and hands the program its turn in between. **Concurrent collection**
runs part of the marking on a separate thread. Together, the two turn one long pause into
many short pauses.

The practical takeaway is this: garbage collection is a performance line item and is
visible on the main thread. The next lesson's diagnostic methods and profiling cover
collection pauses too.

#### Weak References

In some cases you want to reference an object without **keeping it alive**: a cache, an
identity mapping, an observer registry. A strong reference is the wrong tool for this job;
even if the object is no longer used, it cannot be collected because it still stands in
the cache.

Weak structures fill this gap. `WeakMap` and `WeakSet` do not keep their keys alive.

```js
const registry = new FinalizationRegistry((label) => {
  console.log("collected:", label);
});

const strongMap = new Map();
const weakMap = new WeakMap();

function keepStrong() {
  const key = { station: "A1" };
  strongMap.set(key, "value in the strong map");
  registry.register(key, "key of the strong map");
}

function keepWeak() {
  const key = { station: "B2" };
  weakMap.set(key, "value in the weak map");
  registry.register(key, "key of the weak map");
}

keepStrong();
keepWeak();

global.gc();
await new Promise((resolve) => setTimeout(resolve, 10));
global.gc();
await new Promise((resolve) => setTimeout(resolve, 10));

console.log("entry count in the strong map:", strongMap.size);
console.log("collection round finished");
```

```
$ node --expose-gc weak.mjs
collected: key of the weak map
entry count in the strong map: 1
collection round finished
```

Both keys became unreachable once their functions returned. The key in the weak map was
collected; the key in the strong map was not, because the map itself held a strong
reference to it, and the map was itself reachable from the roots.

The rule that follows is direct: **if a mapping's lifetime should depend on the key
object's lifetime, use a weak structure.** Auxiliary data kept per station object in the
measurement stream is an example of this; when the station object drops, its auxiliary
data should drop too.

Weak structures have three limits. Their keys can only be objects (and, since ES2023,
non-registered symbols). They cannot be iterated and their size cannot be queried — because
their content changes depending on the collector's decision, and if that were observable the
program would not be deterministic. `WeakRef` can also hold a single weak reference; but
when a `deref` call will return empty cannot be predicted, so program logic is not built on
it.

#### Measuring the Generational Hypothesis

The generational hypothesis has a directly observable trace: a loop made of short-lived
objects does not permanently grow heap usage. The measurement cannot be done with
absolute numbers — values vary by run, version, and machine — but the **relationship** can
be tested.

```js
function heapStats() {
  const usage = process.memoryUsage();
  return { heapUsed: Math.round(usage.heapUsed / 1024) };
}

const before = heapStats();

for (let i = 0; i < 200000; i++) {
  const temp = { sensor: "S-01", value: i };
  if (temp.value < 0) console.log("unreachable");
}

const after = heapStats();
global.gc();
const afterCollection = heapStats();

console.log("before the short-lived loop (KiB):", before.heapUsed > 0);
console.log("heap after collection smaller than after the loop:",
            afterCollection.heapUsed <= after.heapUsed);
```

```
$ node --expose-gc generation.mjs
before the short-lived loop (KiB): true
heap after collection smaller than after the loop: true
```

Two hundred thousand objects were created and none is reachable after the loop. When
collection is called, heap usage drops back; the cost paid for objects that die in the
young generation is limited to copying the survivors.

Printing a comparison instead of a number is deliberate: absolute byte values differ on
every run and would be misleading if written into the lesson. What is measurable is the
direction of usage.

#### What the Collector Does Not Promise

Three points need to be explicitly known.

The moment of collection is unpredictable. The duration between "became unreachable" and
"memory was given back" is the runtime's decision.

Finalization callbacks are not a guarantee. They may never run while the program is
exiting; for this reason, cleanup of resources like files, connections, and timers is done
with explicit calls.

The collector does not solve leaks. A reachable object, even if unnecessary, is not
collected. A leak is not a collector defect, it is a program defect — and its diagnosis is
therefore sought in the program.

#### Summary

- The mark-and-sweep method is a reachability traversal starting from the roots; its cost
  depends on the number of live objects.
- Reference counting cannot collect cycles; the reachability criterion does, which is why
  mutually referencing objects are not a source of leaks in JavaScript.
- The generational hypothesis rests on the observation that most objects live briefly, and
  it lets the young region be scanned often.
- Collection stops the main thread; incremental and concurrent techniques split a long
  pause into short pauses.
- Weak structures do not keep their keys alive; they are used for mappings whose lifetime
  depends on another object's lifetime.

#### Next Step

Now that how the collector works is known, the cases where it does not help become
recognizable. The next lesson defines and measures leaks: the four most common leak forms
in asynchronous code, what a heap snapshot contains, and the method of counting which
objects accumulate by comparing two snapshots. The measurement stream's leaking and
non-leaking versions will be compared with the same criterion.

### Diagnosing Memory Leaks

Source: https://academia.sh/en/courses/asynchronous-javascript/diagnosing-memory-leaks

A leak defined as unwanted reachability, four common sources in asynchronous code, what a heap snapshot contains, and the method of counting accumulation by comparing two snapshots.

The collector collects every unreachable object. So why does memory fill up? The answer
is in the definition itself: objects that do not get collected are **reachable** ones. A
leak is not memory the collector missed, it is memory the program is holding without
realizing it.

This lesson makes a leak measurable. First the four most common sources in asynchronous
code are recognized, then accumulation is counted with a heap snapshot. The measurement
stream's leaking and bounded versions will be compared with the same criterion.

#### The Definition of a Leak

A **memory leak** is a no-longer-needed object staying reachable from the roots. The
definition has two parts and both are required: the object is unnecessary *and* it is
reachable.

This definition reduces leak hunting to a concrete question: **through which reference
chain is this object reached?** Once the chain is found, the fix is clear too; the line
that breaks the chain is written.

One warning is needed: growing memory is not always a leak. Memory grows while a cache
fills up, and that is by design. The distinguishing criterion is **whether the growth
stops**. A bounded structure levels off at some point; a leak keeps growing linearly.

#### Four Sources in Asynchronous Code

**Unbounded accumulation.** Code that appends every result to an array or map and never
removes anything. This is the most common form in the measurement stream: it starts with
"keep the history around," and because no limit is set, it continues.

**A listener that is not removed.** A listener added to an event source and never removed
holds everything it closes over for as long as the source lives. If the same component
registers repeatedly, the listener count also grows linearly; runtimes warn past a
certain count.

**An uncanceled timer.** A pending timer is a root; every value its callback closes over
stays in memory. A self-scheduling stream keeps this up indefinitely unless it is
stopped.

**An unsettled promise.** A promise that stays pending holds the callbacks bound to it
and the values those callbacks close over.

The last source is worth showing because it is the least visible.

```js
const registry = new FinalizationRegistry((label) => {
  console.log("collected:", label);
});

function pendingWork() {
  const buffer = { name: "measurement buffer" };
  registry.register(buffer, "buffer held by the unsettled promise");
  const promise = new Promise(() => {
    // This promise never settles.
  });
  promise.then(() => console.log("handled:", buffer.name));
  return promise;
}

let pending = pendingWork();

global.gc();
await new Promise((resolve) => setTimeout(resolve, 10));
global.gc();
console.log("collection round finished while reference to the promise persists");

pending = null;
global.gc();
await new Promise((resolve) => setTimeout(resolve, 10));
global.gc();
await new Promise((resolve) => setTimeout(resolve, 10));
console.log("round after the reference was dropped finished");
```

```
$ node --expose-gc pending.mjs
collection round finished while reference to the promise persists
collected: buffer held by the unsettled promise
round after the reference was dropped finished
```

The reference chain is: module variable → pending promise → registered reaction → the
callback's closure → buffer. Once the chain's first link was dropped, the whole thing was
collected.

The practical rule that follows re-justifies the Cancellation and Timeouts lesson from
the memory side: every wait needs a guarantee of ending. A timeout is not just a
user-experience decision, it is a memory decision.

#### Heap Snapshot

A **heap snapshot** is a record of the object graph at a specific moment: nodes are
objects, edges are references. Type, name, and size information is kept for every node.

Two magnitudes are read separately in a snapshot. **Shallow size** is the room the object
occupies by itself. **Retained size** is the total room that would be recovered if the
object were collected — that is, the sum of everything reachable only through that
object. The one that matters in leak hunting is the second: a small object can be holding
up a large structure.

The snapshot is taken from the runtime's debugging interface. It can also be taken from
inside the program; the function below does this and counts how many instances of a
given class are alive.

#### Comparing Two Snapshots

A single snapshot says "there are a lot of objects"; it does not show a leak. The method
is to repeat the same job and compare snapshots:

1. A snapshot is taken before the operation starts — the baseline.
2. The operation is repeated many times.
3. A second snapshot is taken and the difference is examined.

The expectation is that the operation's temporary objects will not be found in the second
snapshot. If they are found, and their count is proportional to the repeat count, there
is accumulation.

```js
import { Session } from "node:inspector/promises";

class Measurement {
  constructor(station, value) {
    this.station = station;
    this.value = value;
  }
}

async function instanceCount(className) {
  const session = new Session();
  session.connect();
  const chunks = [];
  session.on("HeapProfiler.addHeapSnapshotChunk", (message) => chunks.push(message.params.chunk));
  await session.post("HeapProfiler.takeHeapSnapshot", { reportProgress: false });
  session.disconnect();

  const snapshot = JSON.parse(chunks.join(""));
  const fields = snapshot.snapshot.meta.node_fields;
  const typeIndex = fields.indexOf("type");
  const nameIndex = fields.indexOf("name");
  const objectType = snapshot.snapshot.meta.node_types[typeIndex].indexOf("object");

  let count = 0;
  for (let i = 0; i < snapshot.nodes.length; i += fields.length) {
    const type = snapshot.nodes[i + typeIndex];
    const name = snapshot.strings[snapshot.nodes[i + nameIndex]];
    if (type === objectType && name === className) count += 1;
  }
  return count;
}

const history = [];
function leakyHandle(measurement) {
  history.push(measurement);
}

const window = [];
function boundedHandle(measurement) {
  window.push(measurement);
  if (window.length > 10) window.shift();
}

console.log("snapshot 1 — Measurement instances:", await instanceCount("Measurement"));

for (let i = 0; i < 500; i += 1) leakyHandle(new Measurement("A1", 21.4));
console.log("snapshot 2 — after the leaky handler:", await instanceCount("Measurement"));

history.length = 0;
for (let i = 0; i < 500; i += 1) boundedHandle(new Measurement("A1", 21.4));
console.log("snapshot 3 — after the bounded handler:", await instanceCount("Measurement"));
```

```
snapshot 1 — Measurement instances: 0
snapshot 2 — after the leaky handler: 500
snapshot 3 — after the bounded handler: 10
```

The numbers give the diagnosis directly. Five hundred measurements were handled; after
the leaky handler, all five hundred of the five hundred are in memory. In the bounded
handler, only as many objects as the window size — ten — remain, the rest were collected.

The strength of the criterion is in the ratio itself: if instance count grows
proportionally with operation count, the leak is certain. If it sits at a fixed upper
bound, the structure is bounded.

Taking a snapshot itself triggers a garbage-collection round; this is why only truly
reachable objects show up in the count. This is why the count comes out stable.

#### From Diagnosis to Fix

Counting tells you which class is accumulating; it does not tell you the reference
chain. In interface-based tools, the chain is read directly through the "retainers"
view. In a count done from inside the program, the chain is inferred from the code
itself: which structure is holding the object, why that structure is growing.

The fix reduces to one of three patterns. Setting a limit — a window, a bounded cache, a
policy that drops the oldest entry. Removing the link — removing the listener,
canceling the timer, stopping the request. Switching to a weak reference — if a
mapping's lifetime should depend on the key object's lifetime.

#### Summary

- A leak is a no-longer-needed object staying reachable from the roots; it is a program
  defect, not a collector defect.
- There are four common sources in asynchronous code: unbounded accumulation, a listener
  that is not removed, an uncanceled timer, and an unsettled promise.
- A heap snapshot is a record of the object graph; in leak hunting, retained size is more
  informative than shallow size.
- Diagnosis is not made with a single snapshot but by comparing two snapshots taken
  before and after repeating the same operation.
- If instance count grows proportionally with repeat count, there is a leak; if it sits
  at a fixed bound, the structure is bounded.

#### Next Step

The memory side is complete. What remains is the time side: a program can be slow without
leaking memory at all. The next lesson reads the main thread's timeline — the definition
of a long task, what event loop lag means, what a sampling profiler records, and the
observable effect of splitting a long job and yielding turns in between.

### Performance Profiling

Source: https://academia.sh/en/courses/asynchronous-javascript/performance-profiling

Reading the main thread's timeline, the concept of a long task, event loop lag, what a sampling profiler records, and the observable effect of splitting a long job and yielding turns.

A program can perform poorly without leaking memory at all. What the user sees is not a
memory graph, it is an interface that responds late to a click, or a service whose
response time fluctuates.

This lesson's criterion is different from memory: **how long the thread stays locked
onto a single job.** The blocking concept from the course's first lesson becomes
measurable here.

No duration value will be printed throughout the lesson; durations vary by machine and
load and are misleading when written down. What is measured is order, what is shown is
method.

#### Long Task

On every turn, the event loop takes one job from the task queue and runs it to
completion. If this job takes a long time, during that whole time no click is handled, no
response is parsed, no frame is drawn.

Tasks that exceed a given threshold are called a **long task**. The threshold is defined
by the environment; fifty milliseconds is a common bound in browser measurement
interfaces. What matters is not the number itself but the reasoning behind it: for user
interaction to be counted as smooth, the thread has to become free at regular intervals.

A long task's second measure is called **event loop lag**. Its method is direct: a timer
is set up with a known delay, and how much later the callback actually runs is observed.
The gap is the measure of how busy the thread was at that moment. On the server side, this
value is the first indicator that explains fluctuation in response times.

#### Splitting and Yielding

Speeding up a long job is not always possible; but splitting it is possible most of the
time. A split job yields to the event loop between pieces, and pending tasks get to
interleave.

```js
function busyWait(duration) {
  const deadline = Date.now() + duration;
  while (Date.now() < deadline) {
    // Deliberately keeps the thread busy.
  }
}

function yieldTurn() {
  return new Promise((resolve) => setTimeout(resolve, 0));
}

async function processInOnePiece() {
  setTimeout(() => console.log("  interleaving task"), 0);
  busyWait(60);
  console.log("  processing finished");
  await yieldTurn();
}

async function processInPieces() {
  setTimeout(() => console.log("  interleaving task"), 0);
  for (let i = 1; i <= 3; i += 1) {
    busyWait(20);
    console.log("  piece", i, "finished");
    await yieldTurn();
  }
}

console.log("single-piece processing:");
await processInOnePiece();

console.log("split processing:");
await processInPieces();
```

```
single-piece processing:
  processing finished
  interleaving task
split processing:
  piece 1 finished
  interleaving task
  piece 2 finished
  piece 3 finished
```

In single-piece processing, the pending task was left until the very end; in split
processing, it interleaved right after the first piece. The total work is the same —
there is sixty milliseconds of busy time in both cases. What changes is how long the
pending task waits.

Here it is also visible that yielding a turn cannot be done with a microtask. Per the rule
from the Event Loop lesson, the microtask queue is drained completely; `queueMicrotask` or
awaiting an already-settled promise does not yield a turn to the task queue. A real turn
is given only by returning to the task queue.

Splitting has a cost too: as piece count grows, a per-round overhead is paid and total
duration lengthens. The right piece size is the balance between "staying responsive" and
"working efficiently." If the computational load is genuinely heavy, the right answer is
not splitting the response but moving the work to a separate execution context.

#### Measurement Marks

To see where time is spent, the intervals to be measured first need to be named. The
standard measurement interface offers two concepts: a **mark**, which marks a specific
moment, and a **measure**, which names the span between two marks.

```js
import { PerformanceObserver, performance } from "node:perf_hooks";

function busyWait(duration) {
  const deadline = Date.now() + duration;
  while (Date.now() < deadline) {
    // Deliberately keeps the thread busy.
  }
}

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log("entry:", entry.entryType, "—", entry.name, "— was duration measured:", entry.duration > 0);
  }
});
observer.observe({ entryTypes: ["measure"] });

performance.mark("parsing-started");
busyWait(20);
performance.mark("parsing-finished");
busyWait(10);
performance.mark("write-finished");

performance.measure("parsing", "parsing-started", "parsing-finished");
performance.measure("write", "parsing-finished", "write-finished");
performance.measure("total", "parsing-started", "write-finished");

await new Promise((resolve) => setTimeout(resolve, 10));
observer.disconnect();
```

```
entry: measure — parsing — was duration measured: true
entry: measure — total — was duration measured: true
entry: measure — write — was duration measured: true
```

Duration values were not printed; only that the measurement took place was confirmed. The
output order is instructive too: entries are ordered not by creation order but by
**start moment**. The "total" measure was created last, but because it starts from the
same mark as "parsing," it appears second.

Marks' value is that they make measurement embedded in the code and named. In an
asynchronous flow, an operation's start and end happen in different turns; without a
mark, these two moments cannot be tied together by the same measurement.

#### Sampling Profile

Placing a mark works when you know where to look. When you do not, a profiler is used.

The common method is **sampling**: the profiler takes a snapshot of the call stack at
regular intervals. The result tells which function was at the top of the stack in how
many samples; this gives a statistical estimate of the time share falling to each
function.

Two properties of the method determine how the result should be read. The measurement is
approximate — a function that runs very briefly may fall into no sample at all. In
exchange, its cost is low and it does not noticeably change the program's behavior.

The profile is taken from the runtime's debugging interface, and its nodes carry call
frame information.

```js
import { Session } from "node:inspector/promises";

function busyWait(duration) {
  const deadline = Date.now() + duration;
  while (Date.now() < deadline) {
    // Deliberately keeps the thread busy.
  }
}

const session = new Session();
session.connect();
await session.post("Profiler.enable");
await session.post("Profiler.start");

busyWait(60);

const { profile } = await session.post("Profiler.stop");
session.disconnect();

console.log("profile fields:", Object.keys(profile).sort().join(", "));
console.log("was a sample taken:", profile.samples.length > 0);
console.log("is our own function among the nodes:", profile.nodes.some((d) => d.callFrame.functionName === "busyWait"));
```

```
profile fields: endTime, nodes, samples, startTime, timeDeltas
was a sample taken: true
is our own function among the nodes: true
```

The field names explain the profile's structure. `nodes` are the call tree's nodes; each
node represents a function and its call frame. `samples` is, for every sample taken, the
id of the node at the top of the stack. `timeDeltas` is the duration between consecutive
samples. The flame chart interface-based tools draw is nothing more than a visualization
of these three arrays.

The rule for reading the chart follows from this too: **width is duration, height is
call depth.** A wide, short block shows a single function running for a long time; a
narrow, deep stack shows many short calls.

#### What Not to Measure

Three common mistakes render results unusable.

The duration of a single run does not count as a measurement. The first run carries
compilation and cache-warming cost; the measurement has to be repeated and the
distribution examined.

An average alone is not enough. What determines user experience is the outliers; slow
percentiles need to be tracked separately.

An asynchronous flow's "duration" is not the total running time of its functions. Time
spent waiting does not occupy the thread; it is invisible in the profile but real for the
user. For this reason a profile is read together with end-to-end measurement.

#### Summary

- A long task is a task that locks the thread onto a single job and delays every pending
  job; event loop lag is its measure.
- Splitting a long job and yielding to the task queue lets pending jobs interleave; a
  turn cannot be yielded with a microtask.
- The mark and measure interface binds moments falling on different turns in an
  asynchronous flow into named intervals; entries are ordered by start moment.
- A sampling profiler takes a copy of the call stack at regular intervals; its result
  consists of call-tree nodes, samples, and time deltas.
- A single run, an average alone, and profile data alone are all misleading; measurement
  is read together with repetition, distribution, and end-to-end duration.

#### Next Step

Measurement tells you where time is spent, but not what is wrong. The course's last
lesson takes up the second half of diagnostic tools: stopping a program at a specific
point and examining its state. Breakpoints, stepping, watch expressions, and reading
asynchronous call stacks — all will be shown on the same instance of the measurement
stream.

### Debugging Tools

Source: https://academia.sh/en/courses/asynchronous-javascript/debugging-tools

Connecting to the debugging interface, breakpoints and conditional breaks, watch expressions, stepping, and reading asynchronous call stacks.

Profiling told you where time is spent; it did not tell you what is wrong. For that, the
program has to be stopped at a specific point and its variables examined.

This lesson takes up the second half of diagnostic tools. The examples will be built on
the same processing step of the measurement stream; the mechanism used is the same
debugging interface that interface-based tools also run on underneath.

#### The Debugging Interface

The runtime offers an interface that lets a client connected to it stop the program, read
variables, and advance it step by step. Browser developer tools, editor extensions, and
command-line clients all use the same interface.

There are three ways to connect. The runtime is started with the relevant option and a
client connects; it is started with the option that pauses at the start, so it is
tracked from the very first line; or the `debugger` statement is placed inside the code —
if a client is connected, execution stops there.

In the examples below, the program itself is used in place of a client: the same
interface is connected to from inside the program. This shows directly what principles
interface-based tools work on.

#### Breakpoint, Condition, and Watch Expressions

A **breakpoint** is where execution is to be stopped. At the stopping moment, two things
can be examined: local variables' values and the call stack.

Stopping at every breakpoint is useless in a highly repetitive flow. A **conditional
breakpoint** stops only when the given expression is true. The program below achieves
this by evaluating an expression at the stopping moment and continuing right away when
the condition is not met.

```js
import { Session } from "node:inspector";

const session = new Session();
session.connect();
session.post("Debugger.enable");

function readExpression(frame, expression) {
  let result = null;
  session.post(
    "Debugger.evaluateOnCallFrame",
    { callFrameId: frame.callFrameId, expression },
    (error, response) => {
      result = response.result.value ?? response.result.description;
    },
  );
  return result;
}

session.on("Debugger.paused", (event) => {
  const frames = event.params.callFrames;
  const top = frames[0];
  const value = readExpression(top, "measurement.value");

  if (value > 20) {
    console.log("breakpoint:", top.functionName, "— line", top.location.lineNumber + 1);
    console.log("  watch: measurement.station =", readExpression(top, "measurement.station"));
    console.log("  watch: measurement.value =", value);
    console.log("  stack:", frames.slice(0, 3).map((c) => c.functionName).join(" <- "));
  }
  session.post("Debugger.resume");
});

function processMeasurement(measurement) {
  debugger;
  return { station: measurement.station, rounded: Math.round(measurement.value) };
}

function processStream(measurements) {
  return measurements.map(function oneMeasurement(measurement) {
    return processMeasurement(measurement);
  });
}

const result = processStream([
  { station: "A1", value: 21.4 },
  { station: "B2", value: 19.8 },
  { station: "C3", value: 23.1 },
]);

console.log("result:", result.map((s) => `${s.station}=${s.rounded}`).join(" "));
session.disconnect();
```

```
breakpoint: processMeasurement — line 34
  watch: measurement.station = A1
  watch: measurement.value = 21.4
  stack: processMeasurement <- oneMeasurement <- processStream
breakpoint: processMeasurement — line 34
  watch: measurement.station = C3
  watch: measurement.value = 23.1
  stack: processMeasurement <- oneMeasurement <- processStream
result: A1=21 B2=20 C3=23
```

Two of the three measurements met the condition and were reported; on the second
measurement the program continued as if it had never paused at all. The printed line
number is the line the `debugger` statement is on in the source file; if the code
changes, this number changes too.

A **watch expression** is an expression evaluated at the stopping moment. It does not
have to be just a variable name; an expression like `measurement.value > 20`, or even a
method call, can be evaluated too. In interface-based tools, these expressions are written
into a list and recalculated at every stop.

The call stack view comes from the same stop data too: `processMeasurement` was reached
through `oneMeasurement`, which was reached through `processStream`. This chain is the
answer to the question "where was this function called from."

#### Stepping

There are three advancement commands at a stop. **Step over** moves to the next line; it
does not enter called functions. **Step into** stops at the called function's first line.
**Step out** runs until the function currently in returns.

```js
import { Session } from "node:inspector";

const session = new Session();
session.connect();
session.post("Debugger.enable");

let step = 0;

session.on("Debugger.paused", (event) => {
  const top = event.params.callFrames[0];
  console.log("step", step, "— line", top.location.lineNumber + 1);
  step += 1;
  if (step < 8) session.post("Debugger.stepOver");
  else session.post("Debugger.resume");
});

function computeAverage(measurements) {
  debugger;
  let total = 0;
  for (const measurement of measurements) {
    total += measurement.value;
  }
  const average = total / measurements.length;
  return average;
}

const result = computeAverage([
  { station: "A1", value: 21.4 },
  { station: "B2", value: 19.8 },
]);

console.log("average:", result.toFixed(2));
session.disconnect();
```

```
step 0 — line 18
step 1 — line 19
step 2 — line 20
step 3 — line 20
step 4 — line 21
step 5 — line 20
step 6 — line 21
step 7 — line 20
average: 20.60
```

The line sequence exposes the loop's structure: line 20 is the loop header and appears
four times — once as the loop is set up and the iterator is taken, then once per iterator
call: two of them bring a measurement, the last reports that the array is exhausted. Line 21
is the body and runs twice for two measurements.

Stepping has a limit in asynchronous code: when an `await` expression is stepped over,
execution leaves that function and returns to the event loop. Its continuation runs on
another turn, once the relevant promise settles. For this reason, in asynchronous flows,
placing a breakpoint directly at the point of interest is more effective than stepping.

#### Asynchronous Call Stack

As shown in the course's first topic, a callback runs on an empty stack. The diagnostic
consequence of this is heavy: an error occurring inside a callback carries, on its stack,
no information about *who started* that job.

Runtimes close this gap with asynchronous call stacks: the chain of functions bound to
each other with `await` is preserved and appears on the error stack marked with `async`.

```js
function printFrames(error) {
  const frames = error.stack
    .split("\n")
    .slice(1)
    .map((line) => line.trim())
    .filter((line) => !line.includes("node:"))
    .map((line) => line.replace(/\s*\(?file:.*$/, ""))
    .filter((line) => line !== "at async");
  for (const frame of frames) console.log("  ", frame);
}

async function fetchMeasurement(station) {
  await new Promise((resolve) => setTimeout(resolve, 5));
  throw new Error(`station did not respond: ${station}`);
}

async function readMeasurement(station) {
  return await fetchMeasurement(station);
}

async function processStream(stations) {
  const results = [];
  for (const station of stations) {
    results.push(await readMeasurement(station));
  }
  return results;
}

function fetchWithCallback(station, callback) {
  setTimeout(() => callback(new Error(`station did not respond: ${station}`)), 5);
}

try {
  await processStream(["A1"]);
} catch (error) {
  console.log("await chain —", error.message);
  printFrames(error);
}

fetchWithCallback("B2", (error) => {
  console.log("callback chain —", error.message);
  printFrames(error);
});
```

```
await chain — station did not respond: A1
   at fetchMeasurement
   at async readMeasurement
   at async processStream
callback chain — station did not respond: B2
   at Timeout._onTimeout
```

The difference is striking. In the `await` chain, the error's path is visible start to
finish: which station was being read, which stream was being processed, all readable. In
the callback chain, there is only the timer frame; the information of who started the
request has been lost.

The file paths and the runtime's internal frames in the output were filtered out here for
printing; in a real stack, these are present too and vary by machine. Interface-based
tools do the same filtering in the form of "hide library code."

The design rule that follows adds one more to the reasoning from the course's callback
lesson: code written with promises and `await` is not just more readable, it is **more
diagnosable**.

#### Debugging by Printing

Printing to the console looks primitive, but in some cases it is the only workable
method: when the problem occurs rarely, is in production, or stopping itself changes the
behavior.

Two improvements make it a serious tool. First, records **carrying context**: which
station, which attempt, which idempotency key. A context-free "got here" line does not
say which request it belongs to in an asynchronous flow.

Second, using a **logpoint**: it is set up like a breakpoint, but instead of stopping it
prints an expression and continues. This way an observation can be added without changing
or restarting the code.

#### Summary

- The debugging interface offers stopping the program, reading variables, and advancing
  step by step; interface-based tools use this same interface too.
- A breakpoint can be bound to a condition; watch expressions are evaluated at the
  stopping moment, and the call stack shows where the call came from.
- Stepping commands are step over, step into, and step out; when `await` is stepped over,
  execution returns to the event loop.
- An asynchronous call stack preserves the chain bound by `await`; in callback-based code,
  this chain is lost.
- Debugging by printing is a serious tool once records carry context and logpoints are
  used.

#### Course Wrap-Up

The course started with a single question: how does a single-threaded language meet
waiting? The answer was built step by step.

First the **model** was built: user code runs on a single thread, to completion; the
event loop takes one job from the task queue on every turn and then completely drains the
microtask queue. This rule was the reasoning behind every output order in the course.

Then the **abstraction** was built: the gap the callback left — the inability to
compose, manually carrying the error path, contract violations — was closed with the
promise's state machine. Combinators gave waiting its structure, `async` and `await`
fixed how it reads; both were layers sitting on top of the promise, not a new asynchrony
mechanism.

Next, **control** was built: the abort signal, timeouts, and retry patterns. The
distinction here is the course's most practical lesson — cutting the wait does not cut
the job.

Finally, **observation** was built: the reachability-based memory model, leak diagnosis
by snapshot comparison, main-thread profiling, and state inspection with breakpoints. The
measurement stream example went from a single timer callback to a stream that is
cancelable, has a timeout, is resilient, and leaves no leak.

What remains is how this code gets packaged and shipped. The next course — **Modules,
Tooling, and the Ecosystem** — takes up module systems, how packages get resolved,
dependency versioning, and build tools. This course touched on module format a few times
and left the detail for there; its turn has come.

