redux-thunk and handling exceptions inside dispatch results
A Promise.catch
handler also catches any errors thrown from the resolution or rejection handler.
fetch('http://jsonplaceholder.typicode.com/posts').then(res => { throw new Error();}).catch(err => { //will handle errors from both the fetch call and the error from the resolution handler});
To handle only the errors from fetch
and ensure that any error thrown by the call to dispatch({ type: 'FETCH_POSTS_SUCCESS', items: json })
in the resolution handler isn't caught in the catch
handler, attach a rejection handler to fetch
.
return fetch('http://jsonplaceholder.typicode.com/posts').then(response => response.json, error => { dispatch({ type: 'FETCH_POSTS_FAILURE', error: error.message });}).then(json => dispatch({ type: 'FETCH_POSTS_SUCCESS', items: json }), error => { //response body couldn't be parsed as JSON});
fetch
doesn't treat status codes >= 400 as errors, so the above call would only be rejected if there's a network or CORS error, which is why the status code must be checked in the resolution handler.
function fetchHandler(res) { if (res.status >= 400 && res.status < 600) { return Promise.reject(res); } return res.json();}return fetch('http://jsonplaceholder.typicode.com/posts').then(fetchHandler, error => { //network error dispatch({ type: 'NETWORK_FAILURE', error });}).then(json => dispatch({ type: 'FETCH_POSTS_SUCCESS', items: json }), error => { dispatch({ type: 'FETCH_POSTS_FAILURE', error: error.message });});
Please note that any errors thrown in React components may leave React in an inconsistent state, thereby preventing subsequent render
s and making the application unresponsive to UI events. React Fiber addresses this issue with error boundaries.
You could consider moving your error handler into the previous then
block.
I wrote a simple demonstration of the principle: https://codepen.io/anon/pen/gWzOVX?editors=0011
const fetch = () => new Promise((resolve) => { setTimeout(resolve, 100);});const fetchError = () => new Promise((resolve, reject) => { setTimeout(reject, 200)});fetch() .then(() => { throw new Error("error") }) .catch(() => { console.log("error in handler caught") })fetch() .then(() => { throw new Error("error") }, () => { console.log("error in handler not caught") })fetchError() .then(() => { throw new Error("error") }) .catch(() => { console.log("error in fetch caught 1") })fetchError() .then(() => { throw new Error("error") }, () => { console.log("error in fetch caught 2") })
This is a block of code from a fetch wrapper that I wrote. You'll see checkStatus
in the executeRequest
promise chain, which is where I'm checking for any non-2xx responses using response.ok
. Since my API errors return JSON
, I pass any non-2xx responses to parseResponse
and then reject()
the parsed error data which is in turn rejected and returned as an error by executeRequest
:
/** * Parse a reponse based on the type * @param {Response} response * @returns {Promise} <resolve: *, reject: Error> */ const parseResponse = (response) => { const contentType = (response.headers.get('content-type') || '').split(';')[0]; if (contentType === 'application/json') { return response.json(); } else if (contentType === 'multipart/form-data') { return response.formData(); } else if (contentType === 'text/html') { return response.text(); } else if (contentType === 'application/octet-stream') { return response.blob(); } }; /** * Check for API-level errors * @param {Response} response * @returns {Promise} <resolve: Response, reject: Error> */ const checkStatus = (response) => new Promise((resolve, reject) => { if (response.ok) { return resolve(response); } parseResponse(response) .then(reject) .catch(reject); }); /** * Create a new Request object * @param {String} method * @param {String} route * @param {*} [data] * @param {Object} [options] * @returns {Request} */ const buildRequest = (method, route, data = null, definedOptions = {}) => { const options = Object.assign({}, defaultOptions, validateOptions(definedOptions)); const body = () => data ? { body: options.json ? JSON.stringify(data) : data } : {}; const baseOptions = { method: method.toUpperCase(), mode: options.mode, headers: new Headers(headers(options.headers)), }; const requestOptions = Object.assign({}, baseOptions, body()); return new Request(getURL(route), requestOptions); }; /** * Execute a request using fetch * @param {String} method * @param {String} route * @param {*} [body] * @param {Object} [options] */ const executeRequest = (method, route, body, options) => new Promise((resolve, reject) => { fetch(buildRequest(method, route, body, options)) .then(checkStatus) .then(parseResponse) .then(resolve) .catch(reject);