JavaScript Snippet
Fetch with Async/Await Recipes
Difficulty: Medium
The browser and Node 22+ both ship `fetch`, but the easy-to-miss details (`response.ok`, content-type parsing, body shape on POST, parallel error isolation) are exactly where bugs hide. This snippet collects four recipes built on `async`/`await` and a mocked `fetch` so each example runs end-to-end. Use it as the baseline for code that talks to JSON APIs without reaching for axios or a wrapper library.
Fetch with Async/Await Recipes
The browser and Node 22+ both ship `fetch`, but the easy-to-miss details (`response.ok`, content-type parsing, body shape on POST, parallel error isolation) are exactly where bugs hide. This snippet collects four recipes built on `async`/`await` and a mocked `fetch` so each example runs end-to-end. Use it as the baseline for code that talks to JSON APIs without reaching for axios or a wrapper library.
705 views
4
The two non-obvious moves here are checking response.ok and using await response.json(). fetch only rejects on network failures, so a 404 or 500 still resolves successfully and you must inspect response.status (or the boolean shortcut response.ok, which is true for 200-299). response.json() returns a promise of the parsed body, which is why you need a second await after the first. The outer try/catch catches both transport errors and the manual throw, so callers handle one failure shape regardless of whether the network or the server caused it.
POST adds three things over GET: an explicit method, a Content-Type header so the server parses the body correctly, and a serialized body (raw fetch does not stringify objects for you). Forgetting JSON.stringify is a classic bug: the body becomes [object Object] and the server returns 400. Accept: application/json is optional but tells well-behaved servers to skip XML or HTML negotiation. The wrapper pattern reads exactly like the GET version, which is the point: callers see one shape regardless of method.
A Response body can only be read once, and the right reader depends on the payload: json() for JSON, text() for text and HTML, arrayBuffer() (or blob() in browsers) for binary. Inspecting the Content-Type header before picking a reader avoids the common bug where response.json() throws SyntaxError: Unexpected token because the server returned HTML on an error page. Using includes('application/json') rather than strict equality handles parameters like application/json; charset=utf-8 that real servers add. If you call the wrong reader, the body lock is consumed and you cannot retry; reach for response.clone() if you genuinely need two reads of the same body.
Promise.all is the wrong primitive for fan-out fetches because one rejection cancels the whole batch from the caller's perspective (the other requests still complete on the wire, but their results are lost). Promise.allSettled resolves with one descriptor per input and never rejects, so a single 500 does not torpedo the other three users. The status === 'fulfilled' partition keeps successes and errors separate, which is what most product UIs want: render what you got and show a partial-error banner. For tighter throughput limits, pair this with a small pLimit-style concurrency cap so a list of 1000 ids does not open 1000 sockets at once.
