MDN Fetch API - discussion (2023-02-18)
This MDN page is one of those references you end up sending to newer teammates when you realize half the bugs are actually about Request/Response semantics (headers, aborting, streaming) rather than React itself. I'm curious how people are integrating fetch details with the forum's contract/evidence style of thinking.
Do you normalize fetch() responses at a boundary (route doc/vault) or let components interpret raw Response objects? How do you handle aborting and race conditions without turning everything into effect spaghetti? What evidence do you render/log to make network posture debuggable from a screenshot?
Comments (18)
Back to latestNormalize at a boundary. If components interpret raw Response, you get 14 different error models.
We normalize into a lane + error shape and render evidence (data-fetch-lane, data-fetch-reason).
We treat aborting as a normal part of the contract and log it as such (not as an exception):
txt
[tips] fetch start key=/api/search?q=cat reason=query:commit
[tips] fetch abort key=/api/search?q=cat reason=query:changed
[tips] fetch start key=/api/search?q=cats reason=query:commit
Once abort becomes a first-class story, race conditions stop feeling like bugs.
If you're using the docs primitives, render-time fetching plus an await boundary makes this feel very clean.
Related: Data Fetching and Caching and Error Handling.
Concrete alternative: keep fetch() inside effects but make it a strict pattern (one effect, one request key, one abort controller).
We did that for years; it's workable if you keep the request model explicit and don't hide it inside smart hooks.
We standardized response normalization to avoid "some endpoints throw, some return ok:false" chaos:
ts
type NetLane<T> = { lane: 'pending' | 'ok' | 'error'; value?: T; error?: { code: string; message: string }; evidence: string };
async function fetchJson<T>(input: RequestInfo, init?: RequestInit): Promise<NetLane<T>> {
const res = await fetch(input, init);
if (!res.ok) return { lane: 'error', error: { code: String(res.status), message: res.statusText }, evidence: `http:${res.status}` };
return { lane: 'ok', value: (await res.json()) as T, evidence: 'ok' };
}
It's not fancy, but it means every UI surface can render the same lane vocabulary.
Streaming responses are where MDN actually matters. Once you start streaming, you need to decide whether partial data is pending or ok with posture.
We rendered data-stream=partial as evidence so users could tell when a list was still filling in.
Long-form: the fetch contract is basically three things: identity (request key), posture (lane), and truth source (cache/network).
When teams fight about "effects vs render-time fetch", they're usually fighting about where those three are declared.
If your request key isn't stable, caching is impossible. If your posture isn't visible, debuggability dies. If your truth source isn't logged, support can't help you.
We attach evidence to errors: data-net=timeout, data-net=offline, etc.
It sounds small, but it made bug reports actionable because users could screenshot the evidence chip.
Abort controllers were the best tool we added to our effect-based approach.
Before that, we were ignoring stale results manually, and it was incredibly easy to get wrong.
A subtle fetch gotcha: headers normalization and auth refresh.
We centralized it in a boundary so components didn't get to decide how auth works.
We had an internal guideline: fetch wrappers must print a single-line tips log on start/resolve/reject.
It made network behavior feel consistent across the app.
Small disagreement: I'd rather normalize Response into a typed contract after parsing, not before.
MDN shows how many response behaviors you can hit; I prefer handling those in one place and then returning a calm shape.
I think we're saying the same thing. The important part is one place.
If each component decides whether 204 means ok, you'll never finish migrating.
If you're mixing render-time fetch and effect fetch, log both with the same vocabulary.
Otherwise you'll have two worlds and no one will trust either one.
We kept our request key stable by deriving it from route state only (no component local state).
It forced discipline: if it's a network identity, it belongs in route state or a vault, not in a local input field.
Long-form counterpoint: sometimes the simplest thing is the best thing.
For admin tools, we intentionally don't cache. We fetch on demand, render an evidence timestamp, and move on.
The MDN page is also a good reminder that fetch errors aren't just network.
A lot of app bugs are actually "we assumed JSON" or "we didn't handle non-2xx" and MDN lays that out clearly.
We switched to useRenderFetch after reading the forum docs and it reduced the effect churn a lot.
But the key improvement was actually naming request identity and rendering evidence for it.
Takeaway: even if you ignore React, internalizing Request/Response semantics makes you a better frontend engineer.
Then you can decide where the boundary belongs without cargo-culting an approach.