Search code examples
javascriptreactjsreduxredux-promise-middleware

Error handling redux-promise-middleware


I'm learning React, along with pretty much all the necessary technology around it all at once - so I often get tripped up by things I should probably know already.

I've encountered a problem when it comes to error handling my async events. I've scoured the web and nothing really answers exactly what I'm looking for.

I'm currently using redux with redux-promise-middleware to handle the async actions, like this:

export function myFunc() {
  return {
    type: FETCH_FUNC,
    payload: new Promise((resolve, reject) => {
      fetch ('some/url/location/from/which/to/fetch')
        .then( response => {
          if (!response.ok){
            throw new Error(response);
            }
          resolve(response.json());
        }).catch(error => {
          reject(error);
        }),
    })
  };
}

There are two things here: first, the code works just fine when no errors are present. However, when I purposely create an error in the code the correct methods are firing but I still end up with the following error in my console:

Uncaught (in promise) Error: [object Response]

Should the .catch(...) block not be handling this? What am I missing? Should I be getting this anyway? If so, why?

Secondly, I've read that wrapping the fetch inside a new Promise is an anti-pattern, and there was an almost-hint that this may be what's causing problems here. All the examples I've come across use it in this fashion. What's the alternative? How do I fire the resolve/reject to dispatch the next actions without the wrapper?

Any help will be greatly appreciated. Thanks masters of the web.

-------------EDIT 1----------------

From the official redux-promise-middleware github examples, they have the following code:

export default function request(url, options) {
  return new Promise((resolve, reject) => {
    if (!url) reject(new Error('URL parameter required'));
    if (!options) reject(new Error('Options parameter required'));

    fetch(url, options)
      .then(response => response.json())
      .then(response => {
        if (response.errors) reject(response.errors);
        else resolve(response);
      })
      .catch(reject);
  });
}

It seems to intention with the middleware is to wrap fetch inside a new Promise and catching any rejects. If anyone has a working alternative way of implementing this using redux-promise-middleware, or can elaborate on why its following this pattern that would be greatly appreciated.

-------------EDIT 2----------------

Not sure what the intended way of implementing this is or how to avoid the Uncaught error in the promise. Simply calling Promise.reject(...) results in an uncaught error unless you include error handling functions: Promise.reject(...).then(() =>{...}, error => {...}). Including this with the middleware results in the rejected action never being dispatched. I've moved away from redux-promise-middleware till I can find a suitable fix and/or implementation.


Solution

  • I guess what you are getting is the expected result and this is mentioned clearly in the middleware documentation:

    The middleware dispatches rejected actions but does not catch rejected promises. As a result, you may get an "uncaught" warning in the console. This is expected behavior for an uncaught rejected promise. It is your responsibility to catch the errors and not the responsibility of redux-promise-middleware.

    But if you ask about best practices this is what i ended up doing from long time ago and it's working perfectly with me:

    1- For some promises you can do as mentioned in the documentation:

    dispatch({
        type: 'FOO_ACTION',
        payload: new Promise(() => {
          throw new Error('foo');
        })
      }).catch(error => {
        // catch and handle error or do nothing
      });
    

    2- To catch all rejected promises globally add this middleware before the redux-promise-middleware as follow:

    /**
     * a utility to check if a value is a Promise or not
     * @param value
     */
    const isPromise = value => value !== null && typeof value === 'object' && typeof value.then === 'function';
    
    
    export default () => {
    
      const middleWares = [];
    
      // global error middleware
      middleWares.push(() => next => action => {
    
        // If not a promise, continue on
        if (!isPromise(action.payload)) {
          return next(action);
        }
    
        /**
         * include a property in `meta and evaluate that property to check if this error will be handled locally
         *
         * if (!action.meta.localError) {
         *   // handle error
         * }
         *
         * The error middleware serves to dispatch the initial pending promise to
         * the promise middleware, but adds a `catch`.
         */
        if (!action.meta || !action.meta.localError) {
          // Dispatch initial pending promise, but catch any errors
          return next(action).catch(error => {
            if (config.showErrors) { // here you can decide to show or hide errors
              console.log(`${action.type} unhandled rejection caught at middleware with reason: ${JSON.stringify(error.message)}.`);
            }
            return error;
          });
        }
    
        return next(action);
      });
    
      // middleware
      middleWares.push(thunk);
      middleWares.push(promise());  
      middleWares.push(logger());
    
      return applyMiddleware(...middleWares);
    }
    

    i guess this is exactly what you are looking for ;)

    Extra I highly recommend axios over fetch for the following reasons:

    • the axios module automatically reject the promise if the request has an error code which is something you need to keep manually handle in fetch
    • in axios you can create instance with default base-url,header,interceptors ...
    • in axios you can cancel any previous request using a token this is extremely useful specially for autocomplete and chat applications
    • also axios internally automatically switch between xhr and http modules to perform the ajax request based on the environment (NodeJs or Browser), i personally used the same redux actions in electron, nodejs, browser and react-native and it's all working fine