A promise from fetch resolves for any response the server returned, including a 500, and rejects only when the request could not be made at all.
const res = await fetch(url);
if (!res.ok) { // 200-299
throw new HttpError(res.status, await res.text());
}
return res.json();
// a network failure, a CORS rejection or an aborted request rejects.
// a 404 does not.
This is the single most common source of a client that silently treats an error page as data, because res.json() on an HTML error response throws a parse error somewhere unrelated. The res.ok check belongs in a wrapper rather than at every call site, and that wrapper is the natural place to attach the correlation header and the timeout. The rejection also carries almost no detail by design — a CORS failure and a DNS failure are deliberately indistinguishable.