214

Here's what I have going:

import 'whatwg-fetch';

function fetchVehicle(id) {
    return dispatch => {
        return dispatch({
            type: 'FETCH_VEHICLE',
            payload: fetch(`http://swapi.co/api/vehicles/${id}/`)
                .then(status)
                .then(res => res.json())            
                .catch(error => {
                    throw(error);
                })
            });
    };
}

function status(res) {
    if (!res.ok) {
        return Promise.reject()
    }
    return res;
}

EDIT: The promise doesn't get rejected, that's what I'm trying to figure out.

I'm using this fetch polyfill in Redux with redux-promise-middleware.

Vlady Veselinov
  • 4,678
  • 5
  • 23
  • 49
  • 4
    You throw an exception in `catch` but do not `catch` it. – zerkms Jul 07 '16 at 00:08
  • It *does* get to the `catch` (which catches all rejections in the whole chain it is attached to), but the `catch` callback doesn't handle anything - it only rethrows the error. Replace the `throw` with a `console.error` or so. – Bergi Jul 07 '16 at 00:17
  • The browser freezes? That definitely shouldn't happen. – Bergi Jul 07 '16 at 00:18
  • Thanks guys, I'm a bit new to this, the freeze was caused by something else. I think this is an issue for me because the [polyfill](https://github.com/github/fetch) treats a 404 as a successful response. I'm having a bit of trouble rejecting the promise, once I figure that out it should be fine. – Vlady Veselinov Jul 07 '16 at 00:52
  • something more good github.com/github/fetch/issues/203#issuecomment-143347675 – Usman Iqbal Feb 04 '20 at 09:51
  • Anyone else here because the Angular httpClient rejects on 4xx and 5xx responses? – krummens Feb 02 '23 at 19:27

12 Answers12

465

Fetch promises only reject with a TypeError when a network error occurs. Since 4xx and 5xx responses aren't network errors, there's nothing to catch. You'll need to throw an error yourself to use Promise#catch.

A fetch Response conveniently supplies an ok , which tells you whether the request succeeded. Something like this should do the trick:

fetch(url).then((response) => {
  if (response.ok) {
    return response.json();
  }
  throw new Error('Something went wrong');
})
.then((responseJson) => {
  // Do something with the response
})
.catch((error) => {
  console.log(error)
});
gtournie
  • 4,143
  • 1
  • 21
  • 22
fny
  • 31,255
  • 16
  • 96
  • 127
  • 8
    I did not find a property 'ok' instead I checked for response.status === 200. – Torsten Barthel Mar 14 '18 at 03:55
  • 4
    Why can't I tell from my code why the TypeError was thrown? In the console I see in one case it was "net::ERR_CONNECTION_TIMED_OUT" but in another it was "(blocked:mixed-content)" and I don't want to respond the same to both. – Michael Apr 03 '18 at 19:05
  • will this solution stops getting errors in console such as 401 invalid request ? – Shiva Sai Apr 19 '18 at 07:41
  • 9
    How can we return custom responses when there's no network connection or when the server responded with e.g. a `503 Service Temp. Unavailable` if the result of a rejected promise is a `TypeError`? – tonix Jul 11 '18 at 21:29
  • 5
    How can I read a JSON in the catch? I send additional data from the BE that I need in the catch block – Lucas Bustamante Jun 11 '21 at 17:36
  • @LucasBustamante You can throw `JSON.stringify(response.json())` or `JSON.stringify(response.json(), null, 2)` for a prettier output. – M Imam Pratama May 25 '23 at 06:52
  • Recently I can't catch the error that happens because of the network, I did exacly the same you wrote but still I get the predefined error `Failed to fetch` and it doesn't show the error message that I define inside of the then method after the first fetch method – Mehrshad Cheshm Khavari Aug 30 '23 at 07:14
65

The following login with username and password example shows how to:

  1. Check response.ok
  2. reject if not OK, instead of throw an error
  3. Further process any error hints from server, e.g. validation issues
login() {
  const url = "https://example.com/api/users/login";
  const headers = {
    Accept: "application/json",
    "Content-Type": "application/json",
  };
  fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify({
      email: this.username,
      password: this.password,
    }),
  })
    .then((response) => {
      // 1. check response.ok
      if (response.ok) {
        return response.json();
      }
      return Promise.reject(response); // 2. reject instead of throw
    })
    .then((json) => {
      // all good, token is ready
      this.store.commit("token", json.access_token);
    })
    .catch((response) => {
      console.log(response.status, response.statusText);
      // 3. get error messages, if any
      response.json().then((json: any) => {
        console.log(json);
      })
    });
},

ohho
  • 50,879
  • 75
  • 256
  • 383
  • This worked for me! It seems that throw doesn't work like one might be used to in other languages. Simply returning Promise.reject() will pass all subsequent .thens and land in the next catch – maxorcist Oct 29 '21 at 12:46
  • Everywhere I find only approaches in which the pure error codes are written to the console. For a possible internationalization / representation of an error message for the users this is unsuitable. Unfortunately, I have also not yet found a 100% suitable solution. – michaelT Apr 26 '22 at 20:24
28

Thanks for the help everyone, rejecting the promise in .catch() solved my issue:

export function fetchVehicle(id) {
    return dispatch => {
        return dispatch({
            type: 'FETCH_VEHICLE',
            payload: fetch(`http://swapi.co/api/vehicles/${id}/`)
                .then(status)
                .then(res => res.json())    
                .catch(error => {
                    return Promise.reject()
                })
            });
    };
}


function status(res) {
    if (!res.ok) {
        throw new Error(res.statusText);
    }
    return res;
}
Vlady Veselinov
  • 4,678
  • 5
  • 23
  • 49
  • 1
    You could also reject the Promise from your status function as so: ```function status(res) { if (!res.ok) { return Promise.reject(res.statusText); } return res; }``` Or actually you could reject the promise with the message given by your endpoint. – Watchmaker Mar 14 '19 at 15:00
  • Or actually you could reject the promise with the message given by your endpoint if you jsonfy that response and then return a Promise rejected with the properties that you choose from your jsonfied response. – Watchmaker Mar 14 '19 at 15:07
  • 7
    That `.catch(error => { return Promise.reject() })` seems rather pointless. Why suppress the useful `error` and reject with `undefined` instead? – Bergi Sep 26 '19 at 21:52
  • @Bergi The reason why `return Promise.reject()` could be useful inside `catch` is that you can further chain the promise otherwise with a plain catch you would need to check for non empty arg in the `success handler`. – Vivek Apr 28 '20 at 15:43
  • 2
    @Vivek Or you could equally just `throw undefined;`. What I'm complaining about was not the rejection, but ignoring the `error`. Probably the whole thing should be omitted though. – Bergi Apr 28 '20 at 15:50
  • @Bergi Wouldn't `.catch(e=> { console.log(e); return Promise.reject(); })` make sense there for logging the error thrown in success handler and protecting the flow from landing into the next success handlers in case the `catch` is further chained. – Vivek Apr 28 '20 at 15:59
  • 1
    @Vivek That might make more sense, but that's not what they did. Also using `undefined` instead of an error with a proper message is still a bad practice. – Bergi Apr 28 '20 at 16:02
  • What if you want to return error set by the API in the body? res.statusText just gives you the generic http error messages. – David Okwii Apr 12 '21 at 14:15
27

For me, fny answers really got it all. since fetch is not throwing error, we need to throw/handle the error ourselves. Posting my solution with async/await. I think it's more strait forward and readable

Solution 1: Not throwing an error, handle the error ourselves

  async _fetch(request) {
    const fetchResult = await fetch(request); //Making the req
    const result = await fetchResult.json(); // parsing the response

    if (fetchResult.ok) {
      return result; // return success object
    }


    const responseError = {
      type: 'Error',
      message: result.message || 'Something went wrong',
      data: result.data || '',
      code: result.code || '',
    };

    const error = new Error();
    error.info = responseError;

    return (error);
  }

Here if we getting an error, we are building an error object, plain JS object and returning it, the con is that we need to handle it outside. How to use:

  const userSaved = await apiCall(data); // calling fetch
  if (userSaved instanceof Error) {
    debug.log('Failed saving user', userSaved); // handle error

    return;
  }
  debug.log('Success saving user', userSaved); // handle success

Solution 2: Throwing an error, using try/catch

async _fetch(request) {
    const fetchResult = await fetch(request);
    const result = await fetchResult.json();

    if (fetchResult.ok) {
      return result;
    }

    const responseError = {
      type: 'Error',
      message: result.message || 'Something went wrong',
      data: result.data || '',
      code: result.code || '',
    };

    let error = new Error();
    error = { ...error, ...responseError };
    throw (error);
  }

Here we are throwing and error that we created, since Error ctor approve only string, Im creating the plain Error js object, and the use will be:

  try {
    const userSaved = await apiCall(data); // calling fetch
    debug.log('Success saving user', userSaved); // handle success
  } catch (e) {
    debug.log('Failed saving user', userSaved); // handle error
  }

Solution 3: Using customer error

  async _fetch(request) {
    const fetchResult = await fetch(request);
    const result = await fetchResult.json();

    if (fetchResult.ok) {
      return result;
    }

    throw new ClassError(result.message, result.data, result.code);
  }

And:

class ClassError extends Error {

  constructor(message = 'Something went wrong', data = '', code = '') {
    super();
    this.message = message;
    this.data = data;
    this.code = code;
  }

}

Hope it helped.

Tomer Omri
  • 921
  • 9
  • 6
  • 3
    Beware if `fetchResult.ok` is false, the response might not contain json data, in which case you'd get a different exception trying to call `fetchResult.json()` – Andy Mar 02 '22 at 13:10
  • Might be a possibility to check, if the fetchRestult content contains json. Maybe like this: `const contentType = fetchResult.headers.get('content-type') if (contentType && contentType.indexOf('application/json') !== -1) { const result = await fetchResult.json()}` – leonp5 Apr 26 '23 at 08:13
17

2021 TypeScript Answer

What I do is write a fetch wrapper that takes a generic and if the response is ok it will auto .json() and type assert the result, otherwise the wrapper throws the response

export const fetcher = async <T>(input: RequestInfo, init?: RequestInit) => {
  const response = await fetch(input, init);

  if (!response.ok) {
    throw response;
  }

  return response.json() as Promise<T>;
};

and then I'll catch errors and check if they are an instanceof Response. That way TypeScript knows that error has Response properties such as status statusText body headers etc. and I can apply a custom message for each 4xx 5xx status code.

try {
  return await fetcher<LoginResponse>("http://localhost:8080/login", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ email: "user@example.com", password: "passw0rd" }),
  });
} catch (error) {
  if (error instanceof Response) {
    switch (error.status) {
      case 401:
        throw new Error("Invalid login credentials");
      /* ... */
      default:
        throw new Error(`Unknown server error occured: ${error.statusText}`);
    }
  }
  throw new Error(`Something went wrong: ${error.message || error}`);
}

and if something like a network error occurs it can be caught outside of the instanceof Response check with a more generic message i.e.

throw new Error(`Something went wrong: ${error.message || error}`);
Jarod
  • 353
  • 3
  • 7
  • Is it in any way possible to distinguish between 500 and 503 status codes? Often in these cases the variable `error` is not an instance of `Response`, so I have no further information about the source of the error (Server offline – michaelT Apr 26 '22 at 20:22
8

The answer by @fny (the accepted answer) didn't work for me. The throw new Error() wasn't getting picked up by the .catch. My solution was to wrap the fetch with a function that builds a new promise:


function my_fetch(url, args) {
  return new Promise((resolve, reject) => {
    fetch(url, args)
    .then((response) => {
      response.text().then((body) => { 
        if (response.ok) {
          resolve(body) 
        } else {
          reject(body) 
        }
      })
    })
    .catch((error) => { reject(error) })
  })
}

Now every error and non-ok return will be picked up by the .catch method:

my_fetch(url, args)
.then((response) => {
  // Do something with the response
})
.catch((error) => {
  // Do something with the error
})
v4gil
  • 842
  • 1
  • 10
  • 16
4

Another (shorter) version that resonates with most answers:

fetch(url)
.then(response => response.ok ? response.json() : Promise.reject(response))
.then(json => doStuff(json)) //all good

//next line is optional
.catch(response => handleError(response)) //handle error
Alex from Jitbit
  • 53,710
  • 19
  • 160
  • 149
3
function handleErrors(response) {
    if (!response.ok) {
        throw Error(response.statusText);
    }
    return response;
}
fetch("https://example.com/api/users")
    .then(handleErrors)
    .then(response => console.log("ok") )
    .catch(error => console.log(error) );
Mohamed Fadl
  • 1,303
  • 2
  • 8
  • 10
2

I wasn't satisfied with any of the suggested solutions, so I played a bit with Fetch API to find a way to handle both success responses and error responses.

Plan was to get {status: XXX, message: 'a message'} format as a result in both cases.

Note: Success response can contain an empty body. In that case we fallback and use Response.status and Response.statusText to populate resulting response object.

fetch(url)
  .then(handleResponse)
  .then((responseJson) => {
    // Do something with the response
  })
  .catch((error) => {
    console.log(error)
  });

export const handleResponse = (res) => {
  if (!res.ok) {
    return res
      .text()
      .then(result => JSON.parse(result))
      .then(result => Promise.reject({ status: result.status, message: result.message }));
  }
  return res
    .json()
    .then(result => Promise.resolve(result))
    .catch(() => Promise.resolve({ status: res.status, message: res.statusText }));
};
mladzo
  • 1,885
  • 1
  • 18
  • 10
  • What's the purpose of result => Promise.resolve(result). Either result is a Promise or not, but then would handle it either way, wrapping it in a Promise just creates an extra layer that will immediately get unwrapped, no? – Dtipson Nov 23 '22 at 15:29
1

I just checked the status of the response object in particular by using its -ok property that indicates a successfull response (status from 200 - 299) per boolean.

$promise.then( function successCallback(response) {  
  console.log(response);
  if (response.ok) { ... }
});
Torsten Barthel
  • 3,059
  • 1
  • 26
  • 22
  • 2
    Not good enough, 201 (Resource Created) is also a valid response, in fact anything in the range 200-299 is not a client error. – joedotnot Nov 16 '21 at 16:19
  • 1
    Use [`response.ok`](https://developer.mozilla.org/en-US/docs/Web/API/Response/ok): The `ok` read-only property of the `Response` interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not. – M Imam Pratama Jul 06 '23 at 07:46
1

Hope this helps for me throw Error is not working

function handleErrors(response) {
  if (!response.ok) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject({
          status: response.status,
          statusText: response.statusText,
        });
      }, 0);
    });
  }
  return response.json();
}

function clickHandler(event) {
  const textInput = input.value;
  let output;
  fetch(`${URL}${encodeURI(textInput)}`)
    .then(handleErrors)
    .then((json) => {
      output = json.contents.translated;
      console.log(output);
      outputDiv.innerHTML = "<p>" + output + "</p>";
    })
    .catch((error) => alert(error.statusText));

}
Arul
  • 93
  • 1
  • 1
  • 6
0
fetch(url, options)
  .then (
    async (response) => {
      const json = await response.json();
      if (response.ok) {
        console.log(json)
      }
      else {
        console.log(json)
      }
    }
  )
Henshal B
  • 1,540
  • 12
  • 13
  • While this code may answer the question, providing additional context regarding why and/or how this code answers the question improves its long-term value. – Matt Aug 23 '23 at 13:58