How to Fetch with the Fetch API and Typed Responses
Use the Fetch API with typed response parsing so the data has a known shape, and errors include the status and body for debugging.
On this page
Introduction
The hardest part of fetch with types is not writing the code — it is deciding what the code is allowed to reject. A Fetch API setup that accepts everything is easy to ship and expensive to operate, because every malformed input becomes a production surprise. The discipline is to enumerate the shapes the system actually receives, encode them in Fetch API, and let the library reject the rest loudly. This keeps the happy path fast and the failure path visible.
Fetch API earns its place when fetch with types is the bottleneck the user feels. Before that, it is overhead that slows the common case to protect a rare one. Measure the rejection rate and the tail latency, then decide whether the guard is worth its cost. A guard that never fires is a guard you can remove; a guard that fires constantly is a signal that the producer contract is wrong, not that the guard is too strict.
The sections below walk through the concrete decisions: where to put the fetch api check, how to keep it fast under load, what to do when the contract changes, and how to test the failure paths so they degrade on purpose instead of by accident.
Write a typed wrapper
Write a typed wrapper is the first decision because it determines what the rest of the Fetch API pipeline can assume. A wrong choice here turns into fetch with types under load, and the invariant you thought Fetch API guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Write a typed wrapper is the one most teams skip: drive fetch api to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
async function get<T>(url): Promise<T> {
const r = await fetch(url);
if (!r.ok) throw new Error(`${r.status}: ${await r.text()}`);
return r.json() as Promise<T>;
}
The typescript above is the shape to ship for fetch with types. It is small enough to audit and specific enough that the failure behavior is obvious. Adapt the constants to your traffic and latency budget, and resist the urge to handle cases the system has not seen yet.
Parse the JSON
The mechanism behind Parse the JSON is straightforward in Fetch API, but the consequences ripple. A wrong choice here turns into fetch with types under load, and the invariant you thought Fetch API guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Parse the JSON is the one most teams skip: drive fetch api to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Throw on non-2xx
Throw on non-2xx is where most Fetch API implementations leak performance. A wrong choice here turns into fetch with types under load, and the invariant you thought Fetch API guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Throw on non-2xx is the one most teams skip: drive fetch api to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
test('throws on 500', async () => {
await expect(get('/x')).rejects.toThrow('500');
});
Pin the behavior with this test. A Fetch API setup that passes this test degrades on purpose instead of by accident, which is the difference between a managed failure and an incident.
Include the body in errors
The hard part of Include the body in errors is not the happy path — it is what happens when fetch api fails halfway. A wrong choice here turns into fetch with types under load, and the invariant you thought Fetch API guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Include the body in errors is the one most teams skip: drive fetch api to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Retry on network error
Retry on network error should be observable without a debugger. A wrong choice here turns into fetch with types under load, and the invariant you thought Fetch API guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Retry on network error is the one most teams skip: drive fetch api to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Test the wrapper
Test the wrapper is a trade, not a winner. A wrong choice here turns into fetch with types under load, and the invariant you thought Fetch API guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Test the wrapper is the one most teams skip: drive fetch api to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Common pitfalls
The common pitfalls in fetch with types all reduce to one mistake: treating Fetch API as a formality instead of a contract. The contract is the thing the rest of the system relies on, and a weak contract propagates weakness everywhere.
- Skipping the fetch api boundary so fetch with types enters unvalidated.
- Assuming Fetch API is fast without measuring the tail latency under load.
- Letting fetch with types fail silently instead of failing loudly with a metric.
- Testing only the happy path so the fetch api failure surfaces in production.
- Sharing mutable state across requests so one bad input poisons the next.
Conclusion
Do not conflate the conclusion with the implementation. They look similar from the outside but the failure modes differ: Fetch API fails fast and the caller knows, while an unchecked path fails slow and the caller guesses. Choose the one whose failure mode your system can absorb, and document the choice at the call site.
Self-check: write a test that drives fetch api to the failing condition and assert the system degrades gracefully. Then fetch with types is a property you maintain, not a feature you hope for.
Sources: Fetch API, Using Fetch
FAQ
- Why not axios?
- Fetch is built in and tree-shakeable. Use axios only if you need interceptors or older browser support.
- Parse errors?
- Always read the body. A 500 with an empty body is harder to debug.
Report a problem with this articleOpens a pre-filled issue on GitHub.