It’s possible that you still don’t know this:
The Promise returned from
fetch()won’t reject on HTTP error status even if the response is an HTTP
500. Instead, it will resolve normally (with
okstatus set to
false), and it will only reject on network failure or if anything prevented the request from completing. (Fetch API — MDN)
I find myself explaining this during PRs and training sessions more often than not, so I thought that I might as well write it down. Newcomers and less experienced developers tend to struggle with this and there’s no reason to be ashamed.
Seeing is believing
You can test
fetch easily. Create a simple express server like this one:
Point your browser to http://localhost:5000, open the DevTools console, and issue some fetch requests.
You could have expected the
/error/XXX requests to reject due to the server error, but that wasn’t the case. The test code rejected cause we tried to parse as JSON the response content using
res.text(), the call will be resolved to the
Boom! string sent by the server as the error response body:
QED. As the spec says,
fetch only rejects client errors. Server errors are considered successful requests, because, from the
fetch point of view, they are: the server responded with the correct headers and a body, and the connection was closed cleanly 😅.
fetch broken? No, it’s not. This problem is not about what is an error and what’s not. The problem lies in the value returned by
fetch: a Promise.
Promiseobject represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
Completion or failure… 🤔.
POLS: The principle of least surprise
Enter the principle of least surprise (also known as the principle of least astonishment or POLA).
People are part of the system. The design should match the user’s experience, expectations, and mental models.
I think the problem lies in the API and how it clashes with developer expectations. By returning a Promise, one would normally expect it to resolve when the
fetch succeeds and to reject when it fails. A server error, in any mental model is usually a failure. Thus, by only treating client errors as failures,
fetch surprises developers 😌.
What I usually do
If you’ve read some of my other stories you already know. What I usually do is decorate
fetch to make my own custom fetch function that behaves exactly as I (or anyone else would expect):
- The returned promise rejects on fetch errors and when
- It rejects using a common custom Error, that has enough information to discriminate between client and server errors if I need to.
- As a bonus, I make one fetch method per HTTP verb, cause I find it more readable than using the
Using this our tests now behave as expected 🥰.
If you didn’t know this or if you have struggled in the past handling
fetch errors, don’t be ashamed. You are not alone. It has surprised many developers for many years and will continue to do so.