Async, Promises, and fetch
Schedule later work. Await a Promise. Read HTTP once with fetch.
<!-- hal:authoritative:yaml -->
*Schedule work that finishes later. Chain or await a Promise. Read HTTP with fetch, then parse the body once.*
§I — Frame
Duha Primary TypeScript session 6. Topics #10 after #9 generics (shipped 2026-09-18). Bun-centric rows 5–8 (Bun.serve, Bun.file, bun test, @types/bun) stay deferred at this clock. Prior Asr/Duha sessions covered runtime, strict basics, install, modules, and generics. Today the track turns to asynchronous work: how the event loop orders callbacks, how Promise sequences results, how async/await reads like sync code, and how fetch returns a typed future Response.
Done-criteria: Can write a Promise chain, an async function with await + try/catch, and a fetch that reads response.json() (or .text()).
Primary cite: Cherny Programming TypeScript Chapter 8 on disk. Secondary: Bun Fetch for live HTTP, Handbook for the async → Promise return shape.
Home: . Do not dump into Polyglot-Dev/Web/.
§II — Why line order is not time order
Cherny opens with timers: setTimeout(() => console.info('A'), 1), then 'B' at 2 ms, then sync 'C'. Print order is C, A, B, not A, B, C. The main thread calls native async APIs, keeps going, and only later drains the event queue when the call stack is empty.
That model is why Node-style callbacks lie to the eye. fs.readFile and fs.appendFile on the same path can finish in either order. Their type signatures look like ordinary functions: nothing marks them as async. Cherny's point: types alone will not warn you that the read may miss the append. You need the mental model, then better abstractions.
Callbacks remain the primitive. Node APIs often take (err, data) => void as the last argument. Nothing in that signature says "returns later." Cherny's Apache log example runs readFile and appendFile "concurrently" in source order; which finishes first depends on the filesystem. Promises and async/await do not remove the event loop. They make sequencing explicit so you stop pretending adjacent calls are ordered by the wall clock.
§III — Promises sequence futures
A Promise<T> is a value that will settle to T (or reject). Prefer platform Promise over inventing your own state machine; Cherny builds a teaching sketch, then tells you to use the real one.
Chain with .then and recover with .catch:
function appendAndReadPromise(path: string, data: string): Promise<string> {
return appendPromise(path, data).then(() => readPromise(path));
}
getUserID(18)
.then((user) => getLocation(user))
.then((location) => console.info("got location", location))
.catch((error) => console.error(error))
.finally(() => console.info("done getting location"));
Annotate when inference collapses: new Promise<number>((resolve) => resolve(42)) so .then sees number, not {}.
await is language-level sugar for .then. It must sit inside an async function. Prefer try/catch/finally around await instead of a long .catch chain:
async function getUser() {
try {
const user = await getUserID(18);
const location = await getLocation(user);
console.info("got location", location);
} catch (error) {
console.error(error);
} finally {
console.info("done getting location");
}
}
An async function always returns a Promise. Callers can await it or attach .then. That is the Handbook/Cherny contract: sync-looking control flow, still asynchronous under the hood.
§IV — fetch returns a Response future
Bun implements WHATWG fetch. A successful call resolves to a Response. Status is on response.status. The body is read with one of text(), json(), arrayBuffer(), blob(), or formData(). Choose once; do not read the body twice.
const response = await fetch("https://example.com/api/items");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const items: Item[] = await response.json();
POST with a body and headers:
const response = await fetch("https://example.com/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "widget" }),
});
const created = await response.json();
You may pass a Request object instead of a URL string. Bun also documents extras (proxy, unix, tls, timeouts); treat those as optional depth after the WHATWG core works. Do not pull in Bun.serve here; that remains a deferred Bun row.
Type the JSON you expect. response.json() is often any / unknown depending on lib settings: annotate the result (as Item[] or a validated parse) so generics from session 5 stay honest.
Timeouts and cancels: pass signal: AbortSignal.timeout(5000) (or an AbortController) in the init object when the call must not hang forever. That is still WHATWG-shaped; Bun's extra options can wait.
§V — One complete proof
- Explain why
setTimeoutcallbacks can print after a syncconsole.infoon the next line. - Write a
Promisechain with.thenand.catch(or wrap a callback API once). - Rewrite that chain as
async/awaitinsidetry/catch/finally. await fetch(url), checkresponse.ok, thenawait response.json()into a named type.- Send one POST with
method,headers, andJSON.stringifybody.
When those five hold, Topics #10's selected depth is done.
§VI — Closing
Async work is ordered by the event queue, not by source line. Promises name the future value; async/await keeps the chain readable; fetch is the HTTP door that returns a Response you must read once. Cherny supplies the model and the Promise/async spine; Bun's fetch docs supply the live HTTP surface. Bun rows 5–8 stay deferred. Next Primary TypeScript depth follows the syllabus after #10 once a teammate seats it.
Done-criteria: Can write a Promise chain, an async/await + try/catch path, and a fetch that reads the body.