Managing fetch errors with catch() doesn't work [duplicate] - javascript
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.
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)
});
The following login with username and password example shows how to:
Check response.ok
reject if not OK, instead of throw an error
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);
})
});
},
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;
}
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.
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}`);
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
})
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) );
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 }));
};
I just checked the status of the response object:
$promise.then( function successCallback(response) {
console.log(response);
if (response.status === 200) { ... }
});
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));
}
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
Related
Getting unhandled Promise rejection when I already throw err in the catch block
In my redux I got this axios get call As you can see I already throw the err here export const getShippingMethodById = (token, id) => { return (dispatch) => { const config = { headers: { Authorization: `bearer ${token}`, 'Content-Type': 'application/json', }, }; return axios .get(`${baseUri}/api/checkout/GetShippingById?addressId=${id}`, config) .then((res) => { dispatch({ type: FETCH_SHIPPING_METHOD_BY_ID, payload: res.data.shippingInfos, }); }) .catch((err) => { console.log(err); throw err; alert('Cannot connect to server'); }); }; }; In the functional component , inside the useEffect hooks I am calling this function useEffect(() => { let splitText = cartList?.OrderDTO?.DeliveryCountry; let deliveryAddressId = splitText?.split(','); if ( cartList.OrderDTO?.DeliveryCountry !== '' && deliveryAddressId !== undefined ) { dispatch( getShippingMethodById(token.access_token, Number(deliveryAddressId[1])), ).then((res) => { console.log(res); }); } else { dispatch( getShippingMethodById( token.access_token, cartList.OrderDTO?.CustomerAddressId, ), ).then((res) => { console.log(res); }); } }, [cartList]); But when ever this component is loaded I got this error Since I already handle the promise rejection Why am I getting this error?
Because you are using throw err; in catch block. You need to remove it. You can read more about throw in here: https://developer.mozilla.org/vi/docs/Web/JavaScript/Reference/Statements/throw
Throwing an error from a catch won't be caught by the same catch block being executed. This means you are returning a Promise rejection to your UI code and the error should then be caught there. Add a catch to the Promise chain. useEffect(() => { let splitText = cartList?.OrderDTO?.DeliveryCountry; let deliveryAddressId = splitText?.split(','); if ( cartList.OrderDTO?.DeliveryCountry !== '' && deliveryAddressId !== undefined ) { dispatch( getShippingMethodById(token.access_token, Number(deliveryAddressId[1])), ).then((res) => { console.log(res); }).catch(error => { // handle rejected Promise or any other error from this chain }); } else { dispatch( getShippingMethodById( token.access_token, cartList.OrderDTO?.CustomerAddressId, ), ).then((res) => { console.log(res); }).catch(error => { // handle rejected Promise or any other error from this chain }); } }, [cartList]); Or simply remove rethrowing the error: return axios .get(`${baseUri}/api/checkout/GetShippingById?addressId=${id}`, config) .then((res) => { dispatch({ type: FETCH_SHIPPING_METHOD_BY_ID, payload: res.data.shippingInfos, }); }) .catch((err) => { console.log(err); // no error rethrow alert('Cannot connect to server'); });
nested promise reject behaviour
i've some doubt on promise: this is my api functions using axios: const _get = (url: string) => axios .get(url) .then((response: { data: responseData }) => { if (response) { console.log(response) const { data, status, message } = response.data; if (status) { return data } else { throw new Error(message); } } }) //notification is an antd component to show a toast with the error .catch((error: Error) => notification.error({ message: 'Error', description: error.message })); export const doStuff = (id: number) =>_get('/api/do/${id}'); When i call the api in case of error the then() is called const callDoStuff = (id: number) => { doStuff(id).then(() => { //called also if doStuff catch() is resolved notification.success({ message: 'Success', description: 'Template deleted!' }); }); }; so in catch block if i return something is considered resolved and so the outer function then() is called? in this case the only way is to keep the propagation of the error throwing an exception in the catch? Thanks possible soulution: const _get = (url: string) => axios .get(url) .then((response: { data: responseData }) => { if (response) { console.log(response) const { data, status, message } = response.data; if (status) { return data } else { throw new Error(message); } } }) using specific catcher for then() error const callDoStuff = (id: number) => { doStuff(id) .then((response) => {// success handler}, e=>{// specific error thrown by the inner then })}) .catch(e=>{//axios error }) using generic catcher for errors const callDoStuff = (id: number) => { doStuff(id) .then((response) => { //success handler }) .catch(e=>{ // generic error handler })
so in catch block if i return something is considered resolved and so the outer function then() is called? Yes. in this case the only way is to keep the propagation of the error throwing an exception in the catch? I would suggest not to put the .catch() inside _get. Instead, write function callDoStuff(id: number) { doStuff(id).then(() => { notification.success({ message: 'Success', description: 'Template deleted!' }); }, (error: Error) => { notification.error({ message: 'Error', description: error.message }) }); }
fetch api get error messages from server rather than generic messages
I'm using redux thunk to fetch some data in an action function handleErrors(response) { console.log(response) if (!response.ok) { throw Error(response.statusText); } return response; } export const something = (var) => dispatch => { fetch(`${url}/something`, {credentials: 'include'}) .then(handleErrors) .then(res => res.json()) .then(res => dispatch({ type: SOMETHING, payload: res }) ) .catch(error => dispatch({ type: ERROR, payload: error }) ) my express server on an error responds with 'some error' return res.status(500).send({ message: 'some error' }); when it fetches and it's an error (500), its message is the generic "Internal Server Error". how do I get the 'some error' in fetch?
Not sure what’s in your handleError. One approach to extract the error message would be something like this fetch(url) .then(res => { // Check if response has errors if (res.ok) { // No errors return res.json(); } else { // Has errors, since res.json() returns a Promise, we // chain a then here to get the value and return the err value // as Promise rejection so that it will go to the // catch handler return res.json().then(err => Promise.reject(err)); // this could also be // return res.json().then(err => throw Error(err)); } }) .then(json => { // dispatch success }) .catch(err => { // dispatch error });
Fetch: reject promise and catch the error if status is not OK?
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.
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) });
The following login with username and password example shows how to: Check response.ok reject if not OK, instead of throw an error 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); }) }); },
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; }
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.
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}`);
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 })
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) );
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 })); };
I just checked the status of the response object: $promise.then( function successCallback(response) { console.log(response); if (response.status === 200) { ... } });
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)); }
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
HTTP Promise - Handling Errors
I am trying to find a nice way of handling http responses that I consider an error. I am using fetch in React Native. Here is my code. loginRequest(url) { return fetch(url, { method: 'post', headers: { 'Content-Type': 'application/x-www-form-urlencoded;' }, .... }) .then(response => { return this.processResponse(response); }); } Then... processResponse(response) { if (response.status === 200) { return response.json(); } else { let error = new Error(response.status); error.response = response.json(); // This is the problem error.status = response.status; throw error; } }, And the above are called like this: return ApiRequests.loginRequest(username, password) .then(json => { dispatch(Actions.loginSuccess(json, username, password)); }) .catch(error => { dispatch(Actions.loginFailure(error)); }); }; The idea is that I can easily handle all the errors separately (we assume anything but 200 error), within the catch. The problem is that response.json() returns a promise, so assigning it to error.response is not working. I need to keep track of http status code and the response body.
How about this: processResponse(response) { if (response.status === 200) { return response.json(); } else { return response.json().then((data) => { let error = new Error(response.status); error.response = data; error.status = response.status; throw error; }); } }