How to handle errors in fetch() responses with Redux Thunk?
Asked Answered
U

2

19

I'm making API requests using isomorphic fetch, and using Redux to handle my app's state.

I want to handle both internet connection loss errors, and API errors, by firing off Redux actions.

I have the following (work-in-progress / bad) code, but can't figure out the correct way to fire the Redux actions (rather than just throw an error and stop everything) :

export function createPost(data = {}) {

    return dispatch => {

        dispatch(requestCreatePost(data))

        return fetch(API_URL + data.type, {
            credentials: 'same-origin',
            method: 'post',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'X-WP-Nonce': API.nonce
            },
            body: JSON.stringify(Object.assign({}, data, {status: 'publish'}))
        }).catch((err) => {

            //HANDLE WHEN HTTP ISN'T EVEN WORKING
            return dispatch => Promise.all([
                dispatch({type: PRE_FETCH_RESOURCES_FAIL, errorType: 'fatal', message:'Error fetching resources', id: h.uniqueId()}),
                dispatch({type: PRE_CREATE_API_ENTITY_ERROR, errorType: 'fatal', id: h.uniqueId(), message: 'Entity error before creating'})
            ])
        }).then((req) => {

            //HANDLE RESPONSES THAT CONSTITUTE AN ERROR (VIA THEIR HTTP STATUS CODE)
            console.log(req);
            if (!req || req.status >= 400) {
                return dispatch => Promise.all([
                    dispatch({type: FETCH_RESOURCES_FAIL, errorType: 'warning', message:'Error after fetching resources', id: h.uniqueId()}),
                    dispatch({type: CREATE_API_ENTITY_ERROR, errorType: 'warning', id: h.uniqueId(), message: 'Entity error whilst creating'})
                ])
            }
            else {
                return req.json()
            }
        }).then((json) => {
            var returnData = Object.assign({},json,{
                type: data.type
            });
            dispatch(receiveCreatePost(returnData))
        })
    }
}

If I intionally disable the internet connection, in the JS Console, when I log via console.log() (as above), it's outputting this: POST http://example.com/post net::ERR_INTERNET_DISCONNECTED(anonymous function) (dispatch) { return Promise.all([dispatch({ type: PRE_FETCH_RESOURCES_FAIL, errorType: 'fatal', message: 'Error fetching resources', id: _CBUtils2.default.uniqueId() }), dispatch({ type:… cb_app_scripts.js?ver=1.0.0:27976 Uncaught (in promise) TypeError: req.json is not a function(…)

Forgive me if this is entirely wrong, but I don't want to do anything but fire off two Redux Actions when there is an error (a general error, and one specific to the action we were performing when the error occurred).

Is what I'm trying to achieve even possible?

It seems that (via my logging to console) the 'then' part of the script is still being executed (as the contents of it are my 'catch' dispatch functions)..

Unvarnished answered 6/5, 2016 at 17:27 Comment(0)
S
56

I’m confused about several things:

  1. Why do you use Promise.all around dispatching two synchronous actions? Calling dispatch with something like {type: PRE_FETCH_RESOURCES_FAIL, ...} won’t return a Promise, so Promise.all is unnecessary. Promise.all() is only useful if the actions you dispatch are themselves written as thunk action creators, which is not the case here.
  2. return dispatch => ... is only necessary once at the very beginning of the action creators. There is no need to repeat this in the catch or then blocks—in fact, repeating it makes the inner code not execute at all. This is a way to inject dispatch into your function at the top level, and there is no point to repeating it.
  3. If you put then after a catch, it will run even after an error was caught. This is not the behavior your want—it doesn’t make sense to run the success handler right after the error handler. You want them to be two separate code paths.
  4. Minor naming nitpick: you are calling the response a “req”. It should probably be res.

It feels like you have a wrong mental model of how Redux Thunk works, and are trying to combine parts of different examples together until it clicks. The random indentation also contributes to this code being a little bit hard to understand.

This is going to be painful in the future so instead I suggest to get a more complete mental model of what Redux Thunk does, what return dispatch => ... means, and how Promises fit into the picture. I would recommend this answer as an in-depth introduction to Redux Thunk.

If we fix those problems, your code should look roughly like this instead:

export function createPost(data = {}) {
  return dispatch => {
    dispatch(requestCreatePost(data));

    return fetch(API_URL + data.type, {
      credentials: 'same-origin',
      method: 'post',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'X-WP-Nonce': API.nonce
      },
      body: JSON.stringify(Object.assign({}, data, {status: 'publish'}))
    })
    // Try to parse the response
    .then(response =>
      response.json().then(json => ({
        status: response.status,
        json
      })
    ))
    .then(
      // Both fetching and parsing succeeded!
      ({ status, json }) => {
        if (status >= 400) {
          // Status looks bad
          dispatch({type: FETCH_RESOURCES_FAIL, errorType: 'warning', message:'Error after fetching resources', id: h.uniqueId()}),
          dispatch({type: CREATE_API_ENTITY_ERROR, errorType: 'warning', id: h.uniqueId(), message: 'Entity error whilst creating'})
        } else {
          // Status looks good
          var returnData = Object.assign({}, json, {
              type: data.type
          });
          dispatch(receiveCreatePost(returnData))
        }
      },
      // Either fetching or parsing failed!
      err => {
        dispatch({type: PRE_FETCH_RESOURCES_FAIL, errorType: 'fatal', message:'Error fetching resources', id: h.uniqueId()}),
        dispatch({type: PRE_CREATE_API_ENTITY_ERROR, errorType: 'fatal', id: h.uniqueId(), message: 'Entity error before creating'})
      }
    );
  }
}
Sty answered 8/5, 2016 at 12:4 Comment(4)
Wow, thanks for the detailed dissection. Reading the Redux Thunk intro' right now. Thanks a lot!Unvarnished
@Dan Abramov what if I want to extract the fetch into a separate place, together with the "fetch completely failed (say cors or connection timeout/refused)" catch, but leave the more specific catch in the action. Is it possible?Durer
@Dan Abramov - I am having issues chaining off of a promise in this scenario. it is always firing the resolved function in the chain instead of the rejected one, when in the thunk it is definitely rejected since my error action is being dispatched. thoughts?Leigha
update, my fault. I ended up returning a promise that wraps my api call, which returned the promise. since the promise was being handled, I was .then() ing off of the promise outside the chain, so success was called since the err was already handled.Leigha
U
-2

The solution was simply to (for both instances of error logging) replace:

return dispatch => Promise.all([
    dispatch({type: PRE_FETCH_RESOURCES_FAIL, errorType: 'fatal', message:'Error fetching resources', id: h.uniqueId()}),
    dispatch({type: PRE_CREATE_API_ENTITY_ERROR, errorType: 'fatal', id: h.uniqueId(), message: 'Entity error before creating'})
])```

With:

return Promise.all([
    dispatch({type: PRE_FETCH_RESOURCES_FAIL, errorType: 'fatal', message:'Error fetching resources', id: h.uniqueId()}),
    dispatch({type: PRE_CREATE_API_ENTITY_ERROR, errorType: 'fatal', id: h.uniqueId(), message: 'Entity error before creating'}),
Promise.reject(err)
])
Unvarnished answered 8/5, 2016 at 11:44 Comment(1)
Both Promise.all and Promise.reject are unnecessary. While you can technically make it work, this is making the code more complicated than it needs to be, and serves no purpose. Please see my answer for details and a rewrite suggestion.Sty

© 2022 - 2024 — McMap. All rights reserved.