I have a basic thunk action creator and reducer adapted from the Redux documentation: http://redux.js.org/docs/advanced/AsyncActions.html
// action creator
function fetchPosts () {
return dispatch => {
dispatch({ type: 'FETCH_POSTS_REQUEST' })
return fetch('http://jsonplaceholder.typicode.com/posts')
.then(response => response.json())
.then(json => dispatch({ type: 'FETCH_POSTS_SUCCESS', items: json }))
// THIS CATCHES FETCH REQUEST ERRORS, AND COMPONENT LEVEL ERRORS
.catch(error => dispatch({ type: 'FETCH_POSTS_FAILURE', error: error.message }))
}
}
// reducer
function reducer (state = { isFetching: false, items: [] }, action) {
switch (action.type) {
case 'FETCH_POSTS_REQUEST':
return Object.assign({}, state, { isFetching: true })
case 'FETCH_POSTS_SUCCESS':
return Object.assign({}, state, { isFetching: false, items: action.items })
case 'FETCH_POSTS_FAILURE':
return Object.assign({}, state, { isFetching: false })
default:
return state
}
}
In a React component that is passed the state as props, I check for the presence of post items, and if present force an component level error:
const Test = props => {
if (!props.items.length) return null
throw new Error('Error!')
}
When starting the app:
fetchPosts
action creator is calledprops.invalidProperty.error
Cannot read property 'error' of undefined
So far so good.
The issue is that the JS exception from the component is never output to the console. Instead, the catch()
block for the fetch promise catches the error, and dispatches an FETCH_POSTS_FAILURE action.
This has the effect of swallowing all errors in components that were affected by updating the store. A FETCH_POSTS_FAILURE state change is dispatched, but this feels incorrect - there was no error actually fetching the posts, but an error downstream in a component using those posts.
I'm looking for a pattern to help separate errors in the async request from any other random error that occurs as a result of changing the state via dispatch.
EDIT:
Example with the async example in the Redux github repo: https://github.com/nandastone/redux/commit/88ab48040ce41c39d8daba8cc0c13a6f32c38adf#diff-eeb827d44ad03655e63b7e9319a03dd4R6
Where does this new dispatch argument come from? The short answer is that the redux-thunk middleware has access to the store, and can therefore pass in the store's dispatch and getState when invoking the thunk. The middleware itself is responsible for injecting those dependencies into the thunk.
The app code dispatches an action to the Redux store, like dispatch({type: 'counter/incremented'}) The store runs the reducer function again with the previous state and the current action , and saves the return value as the new state.
Redux Thunk is a middleware that lets you call action creators that return a function instead of an action object. That function receives the store's dispatch method, which is then used to dispatch regular synchronous actions inside the function's body once the asynchronous operations have been completed.
By itself, a Redux store doesn't know anything about async logic. It only knows how to synchronously dispatch actions, update the state by calling the root reducer function, and notify the UI that something has changed. Any asynchronicity has to happen outside the store.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With