Typorithm · hard

2. Typed fetch, status failures and decoding

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

2. Typed fetch, status failures and decoding

0%

Concept and contract

Connect transport handling to a decoder rather than asserting a generic response type.

Fetch resolves a Response for HTTP failures such as 404 or 500; those statuses do not automatically reject the promise. Network failure, HTTP status failure, JSON parsing failure and schema failure are different events and may deserve different recovery behavior.

A small transport helper should accept a decoder that establishes T from unknown input. This makes its generic output relationship justified by executable evidence. It also keeps the transport layer reusable without claiming every endpoint returns the same shape.

Mechanism and worked example

The example injects a minimal fetch-like function for deterministic tests.

The example injects a minimal fetch-like function for deterministic tests. It checks ok before reading JSON, assigns the parsed value to unknown, and calls the decoder. A missing decoded result becomes a payload error instead of a successful arbitrary T.

Real fetch integration also needs cancellation, credentials policy and safe URL construction. These are intentionally separate from the basic helper. Do not add automatic retries for every failure; retrying malformed payloads or non-idempotent writes can make a problem worse.

Complete worked example

typescript

type ResponseLike = { ok: boolean; json(): Promise<unknown> };
async function load<T>(request: () => Promise<ResponseLike>, decode: (raw: unknown) => T | undefined): Promise<T> {
  const response = await request();
  if(!response.ok) throw new Error("HTTP failure");
  const value = decode(await response.json());
  if(value === undefined) throw new Error("Invalid payload");
  return value;
}
Reasoning
The decoder, not a caller assertion, justifies the helper’s successful generic output.

Counterexamples and common mistakes

The rejected decoder returns text where the declared output requires an object.

The rejected decoder returns text where the declared output requires an object. This protects the relationship between executable decoder and consumer result. A helper returning raw as T would bypass that relationship and allow contradictory caller expectations.

Avoid displaying raw server bodies or caught objects to users. A production client can map stable error categories to messages while retaining bounded private diagnostics. Keep request tokens, personal data and full payloads out of ordinary logs.

Rejected counterexample: expected compiler diagnostic

typescript

type Role = { id: string };
// @ts-expect-error This decoder does not establish Role.
const decode: (raw: unknown) => Role | undefined = raw => "not a role";
Reasoning
A promised output type must correspond to an actual decoder result.

Independent practice

Implement loadCount using an injected response with ok and json.

Implement loadCount using an injected response with ok and json. Require a successful status and a decoded finite nonnegative integer. Begin with a valid response, then add status and malformed-payload branches separately.

Test a successful zero count, HTTP failure and a string count. Optional extension: add an AbortSignal to the injected transport contract. The basic task remains a short boundary function with three observable outcomes.

Step 1: Handle HTTP status before parsing. Step 2: Validate the count from unknown JSON.

Practice starter: solve before reading the solution

typescript

type ResponseLike = { ok: boolean; json(): Promise<unknown> };
async function loadCount(request: () => Promise<ResponseLike>): Promise<number> { throw new Error("TODO"); }
Reasoning
Add status handling and payload validation as separate small steps.
  • Hint 1: Zero is valid; negative and fractional values are not.
  • Hint 2: A successful HTTP response can still contain an invalid payload.

Solution, tests and trade-offs

The solution checks status before parsing and validates the numeric payload explicitly.

The solution checks status before parsing and validates the numeric payload explicitly. Its async return type reflects the successful value, while rejected promises carry failure behavior outside ordinary TypeScript return unions. Callers must still catch or otherwise handle rejection.

Execute the three deterministic responses and assert the resulting values or rejections. A compiler test verifies that the decoder result matches the promised DTO. These tests are independent of a live network, so provider downtime cannot make the lesson flaky.

Practice solution and acceptance checks

typescript

type ResponseLike = { ok: boolean; json(): Promise<unknown> };
async function loadCount(request: () => Promise<ResponseLike>): Promise<number> {
  const response = await request(); if(!response.ok) throw new Error("HTTP failure");
  const value = await response.json();
  if(typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new Error("Invalid count");
  return value;
}

// Acceptance checks
void (async()=>{if(await loadCount(async()=>({ok:true,json:async()=>0}))!==0)throw new Error("zero");for(const response of [{ok:false,json:async()=>2},{ok:true,json:async()=>"2"}]){let bad=false;try{await loadCount(async()=>response)}catch{bad=true}if(!bad)throw new Error("rejection");}})();
Reasoning
Status and numeric validation produce distinct failure points even though both reject the promise.

Interview reasoning and next step

In interviews, explain why await fetch can succeed for a 500 response and why await response.json does not validate an interface.

In interviews, explain why await fetch can succeed for a 500 response and why await response.json does not validate an interface. Show where each error category originates and whether retrying it is appropriate.

Continue with serialization contracts. Even correctly decoded in-memory values can change meaning when converted to JSON, especially undefined, null, dates and identifiers represented by large integers.

Optional stretch — after the core task works: Thread an AbortSignal through the injected transport.

Connect transport handling to a decoder rather than asserting a generic response type.