Fetch API Errors and the Principle of Least Surprise
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 HTTP404
or500
. Instead, it will resolve normally (withok
status set tofalse
), 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.json()
. 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 😅.
So… is 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.
The
Promise
object 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 😌.
Some languages, like Ruby, were designed with POLS in mind, and are approachable and a joy to work with. JavaScript is not one of those languages.
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
response.ok
isfalse
. - 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
method
option 🤷🏽♀️.
Using this our tests now behave as expected 🥰.
Summary
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.