MDN Fetch API - discussion (2023-01-24)
MDN's Fetch API docs are still the clearest place to point when a React discussion becomes "fetch is weird". I'm curious how people map fetch primitives (abort, response types, cloning, cache semantics) onto route-first apps where data reads are part of the render contract and you want the UI to be explainable via evidence rather than timing.
Where do you put AbortController in your app architecture (per route, per request key, per panel)? Do you treat caching as a browser concern or as an app posture (cache lane) that you render as evidence? If a request fails, what do you store/render so you can debug without opening devtools?
Comments (18)
Back to latestAbort is best when identity changes. If the user switched location/query/tenant, the old request should die.
We attach a request key and store it as evidence so "which response applied" is clear:
ts
type Req = { key: string; url: string; startedAt: number };
function startRequest(doc: any, url: string) {
const key = `r-${Date.now()}`;
doc.write('request', { key, url, startedAt: Date.now() } as Req);
return key;
}Counterpoint: abort can hide bugs if you use it as a race-condition bandaid.
We only abort on identity change; we still validate response application by key.
Same. Abort is necessary but not sufficient.
If you don't have a "does this response still match" check, you're still at risk.
Caching became manageable once we treated it as posture: cacheLane=bypass|cache and we rendered it as evidence in the shell.
Otherwise you can't tell if stale data was expected or a bug.
Error evidence we store: status code, error class, and last successful timestamp.
If you only store error=true, you can't debug anything.
MDN's details on response body usage (json/text/arrayBuffer) mattered for us when we started storing previews.
We clone the response for debug previews only in dev builds so we don't pay the cost in prod.
Streaming: we stored partial chunks in the document and rendered a data-stream-progress number.
If you don't store partial truth, rerenders reset the story.
A misunderstanding I had: I thought fetch errors were just network errors.
Parse errors and opaque responses are their own class and should be recorded as evidence differently.
Docs tie-in: fetch becomes less mystical when you pair it with a data fetching posture and a consistent evidence surface.
Related: Data Fetching and Caching (Best Practices) and Testing and Debugging.
We centralize the fetch client (headers/timeouts) but let panels choose freshness.
Consistency in the client, flexibility in posture.
If you want fewer flakes in tests, store request status as a lane (pending|ok|error|aborted) and render it.
Then tests wait for the lane change instead of guessing timing.
Counterpoint: rendering too much network evidence can be noisy for users.
We keep it behind a debug flag and only show the minimal bits in UI (retry button, status).
We store a derived freshnessAt timestamp per panel.
It turned "why is this stale" into an answerable question.
The best thing we did was stop doing "setState from fetch effect" and instead write into a document/vault surface that is already part of the route contract.
It made updates predictable.
If your app does duplicate reads, you need a rule for merging and you need to make the winner visible.
Otherwise you get "it depends" bugs that nobody can reproduce.
We treat abort as a normal state, not an error.
If you lump it into errors, monitoring becomes useless because you can't distinguish cancellations from failures.
I still recommend MDN to juniors because it covers the primitive details (what fetch returns, what errors look like).
Framework docs often skip that and then you get confused when debugging.
Fetch isn't hard, but debugging invisible fetch behavior is. Evidence + posture is the fix.