Typed Fetch & Error Handling

Advanced · 20 min read · ▶ live playground · ✦ checkpoint

Typing fetch means typing the promise and validating the JSON before your app trusts it. The trap is res.json(): in TypeScript's DOM declarations, its result behaves like any, so a generic fetchJson<T> can compile while proving nothing.

The network is the same runtime boundary you defended in Section 13. Async does not make the boundary safer; it only wraps the boundary in a promise.

res.json() is where the lie enters

fetch is the standard JavaScript API for making network requests. It gives you a Promise<Response>, and then response.json() parses the response body.

Here is read-only reference code. It is the shape you see in real apps, but this page's live playgrounds do not call the network:

type LessonSummary = {
  slug: string;
  minutes: number;
};
 
async function loadLessonReference(slug: string): Promise<LessonSummary> {
  const res = await fetch("/api/lessons/" + slug);
  const lesson: LessonSummary = await res.json();
  return lesson;
}
 
async function fetchJson<T>(url: string): Promise<T> {
  const res = await fetch(url);
  return res.json();
}

That compiles because the JSON result is effectively "trust me." const lesson: LessonSummary = await res.json() looks like validation, but it is only an annotation placed after untrusted data has crossed the line.

Now watch the same shape without a real network call:

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

→ minutes plus one: soon1

The break-it line proves the checker believes the fiction: lesson.minutes is typed as number, so assigning it to string fails. The runtime output proves the data never signed that contract.

Decode unknown before returning typed data

A decoder is a function that checks an unknown value at runtime and returns typed data only if the check succeeds. A Result is a discriminated union that carries either success data or a typed failure.

This playground uses a simulated response whose json() returns unknown. That is the honest starting point: payloads cross the boundary first, and types are earned after parsing.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

→ loaded typed-fetch (20 min)
→ blocked: minutes must be a number

The success path now has a receipt: decodeLesson checked the payload and built the LessonSummary your app may use. The failure path is data too, so callers cannot touch .data until they check ok.

14.3 will slow down on this Result<T, E> pattern. For now, keep the boundary rule: unknown in, decoder result out.

Catch variables start as unknown

Network code fails in two ways: responses can be bad, and the request itself can reject. A caught variable is the value bound in catch (error); with the course's strict settings, useUnknownInCatchVariables makes that value unknown.

Use instanceof Error for normal errors. Use an error predicate, a function returning value is SomeErrorShape, when a library throws structured objects.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

→ RATE_LIMITED: try again later
→ offline

This is not extra ceremony. It is the same shape every time: the boundary gives you unknown; your code proves the useful cases; the rest gets a safe fallback.

Next lesson turns that local Result<T, E> into a full error-handling style: expected failures as values, unexpected failures as exceptions, and a rule for choosing between them.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion