The problem

If you've been using the fetch() function in the browser or modern versions of Node.js, you know that it returns a Promise.

1fetch(request).then((response) => {
2 // Handle the response.
3})

You also know that promises in JavaScript reject if there's been an error while resolving them. But, for some obscure reason, the Promise returned from fetch() never rejects when we get an error response. Not only is that confusing but it also forces you to explicitly check the response status before handling the response further:

1fetch(request).then((response) => {
2 if (!response.ok) {
3 throw new Error(`Server responded with ${response.statusCode}`)
4 }
5
6 return response.json()
7})

Okay, before you add this behavior to the list of JavaScript oddities, let me assure you that it's entirely correct. You think of it as unexpected simply because the fetch Promise doesn's stand for what you think it does. Let me explain.

The promise

Naturally, when a request returns a promise we expect its fulfillment state to reflect the state of that request:

  • The promise resolves if the request is successful;
  • The promise rejects if it the request is not successful (i.e. failed).

And that's precisely what happens! With the exception that you're likely misinterpreting what a "successful request" actually is from the network code's perspective.

The network code

Here's a request/response transaction represented in a somewhat simplified list of steps:

  1. A request is triggered in your code;
  2. Request headers are sent to the server. Keep in mind, I'm talking about the HTTP message headers, which is this:
1GET https://kettanaito.com HTTP/1.0
2accept: text/html;charset=UTF-8
3# ...the rest of the request headers, excluding the body.
  1. Request body starts streaming to the server.
  2. Request body finishes streaming.
  3. The server sends the response headers.
  4. Response body finishes streaming to the client.

From the network code's perspective, the request is successful once it has been successfully sent to the server in its entirety (passed #3 in the list above). Although it's a bit counter-intuitive, request's success has nothing to do with the kind of response you get from the server.

As long as the outgoing request was successfully parsed and sent to the server, it is considered successful.

This means that the Promise returned by the fetch() function is, in fact, a request Promise, and it will only reject if the request itself failed.

Error responses

Bear in mind that error responses do not indicate failed requests. For your application to get an error from the server, it must reach that server, which means it must successfully send a request and get something back.

This is, once again, a confusion of intentions. As developers, our intention when making a request is to get a "happy path" resolution for that request. If we fetch GET /puppies, we expect to get the list of puppies. Anything else is considered unexpected, an error.

But fetch() cannot really assume that. Consider the case when we actually expect an error response instead of 200 OK. Suddenly, our expectations toward the response are context-dependent and aren't something fetch should concerns itself with.

The rejections

Despite popular belief, the fetch() Promise does reject, and here's when:

  • Incorrectly constructed request;
  • A network error (e.g. DNS lookup failures, unreachable network);
  • The request has been aborted.

And these are precisely the cases you should handle in the .catch() closure of your request Promise:

1fetch(request)
2 .then((response) => {
3 // Handle the response.
4 })
5 .catch((error) => {
6 // Handle request errors.
7 })

Wait, but this is actually pretty neat! If the request has been succesful, we proceed with handling its response (whatever it is) in the .then() callback, and if the request itself failed, we can capture and handle that error in the .catch() callback.

The Fetch API is one of the well-designed APIs on the web, and it's the details like this that show it. Remember that before you think something odd, try diving a bit deeper to understand it and, perhaps, the oddities of the past will become the discoveries of the future.