tl;dr; You can use error instanceof window.Response
to distinguish between fetch
exceptions and fetch
responses.
When you do something like...
const response = await fetch(URL);
...two bad things can happen.
- The XHR request fails entirely. I.e. there's not even a response with a HTTP status code.
- The response "worked" but the HTTP status code was not to your liking.
Either way, your React app needs to deal with this. Ideally in a not-too-clunky way. So here is one take on this challenge/opportunity which I hope can inspire you to extend it the way you need it to go.
The trick is to "clump" exceptions with responses. Then you can do this:
function ShowServerError({ error }) {
if (!error) {
return null;
}
return (
<div className="alert">
<h3>Server Error</h3>
{error instanceof window.Response ? (
<p>
<b>{error.status}</b> on <b>{error.url}</b>
<br />
<small>{error.statusText}</small>
</p>
) : (
<p>
<code>{error.toString()}</code>
</p>
)}
</div>
);
}
The greatest trick the devil ever pulled was to use if (error instanceof window.Reponse) {
. Then you know that error
thing is the outcome of THIS = await fetch(URL)
(or fetch(URL).then(THIS)
if you prefer). Another good trick the devil pulled was to be aware that exceptions, when asked to render in React does not naturally call its .toString()
so you have to do that yourself with {error.toString()}
.
This codesandbox demonstrates it quite well. (Although, at the time of writing, codesandbox will spew warnings related to testing React components in the console log. Ignore that.)
If you can't open that codesandbox, here's the gist of it:
React.useEffect(() => {
url &&
(async () => {
let response;
try {
response = await fetch(url);
} catch (ex) {
return setServerError(ex);
}
if (!response.ok) {
return setServerError(response);
}
// do something here with `await response.json()`
})(url);
}, [url]);
By the way, another important trick is to be subtle with how you put the try {
and } catch(ex) {
.
// DON'T DO THIS
try {
const response = await fetch(url);
if (!response.ok) {
setServerError(response);
}
// do something here with `await response.json()`
} catch (ex) {
setServerError(ex);
}
Instead...
// DO THIS
let response;
try {
response = await fetch(url);
} catch (ex) {
return setServerError(ex);
}
if (!response.ok) {
return setServerError(response);
}
// do something here with `await response.json()`
If you don't do that you risk catching other exceptions that aren't exclusively the fetch()
call. Also, notice the use of return
inside the catch block which will exit the function early leaving you the rest of the code (de-dented 1 level) to deal with the happy-path response object.
Be aware that the test if (!response.ok)
is simplistic. It's just a shorthand for checking if the "status in the range 200 to 299, inclusive". Realistically getting a response.status === 400
isn't an "error" really. It might just be a validation error hint from a server, and likely the await response.json()
will work and contain useful information. No need to throw up a toast or a flash message that the communication with the server failed.
Conclusion
The details matter. You might want to deal with exceptions entirely differently from successful responses with bad HTTP status codes. It's nevertheless important to appreciate two things:
-
Handle complete
fetch()
failures and feed your UI or your retry mechanisms. -
You can, in one component distinguish between a "successful"
fetch()
call and thrown JavaScript exceptions.