Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is my http library's error handler catching *all* runtime errors in the app?

I'm building a prototype app using React Native 0.39 that requests some data from a remote source. To make the request, I use Axios.

The call seems pretty straightforward. In a component named TownsList.js, I do

class TownsList extends Component {

  constructor(props) {
    super(props);
    this.state = {towns: []};
  }

  componentDidMount() {

    let url = "(SOME URL HERE)";

    axios.get(url)
         .then(function(response) {
           // Do stuff witt the successful result
         })
         .catch(function (error) {
           Alert.alert(
             'Download failed',
             'Unable to download data from '+url+"\nError:"+error,
             [{text: 'OK'}])
         });

...

The weird thing now is that whenever I have some other runtime error in my code within // Do stuff witt the successful result block - for example an incorrectly cased reference to some constant or variable - that error will be handled by Axios' error handler, as well:

enter image description here

That doesn't feel right. What am I doing wrong? Should I set up "generic" error handling somewhere else in my app to catch these things? Or is this intended behaviour?

like image 683
Pekka Avatar asked Jan 01 '17 10:01

Pekka


2 Answers

This is the natural behavior if an error is thrown within the block you marked as

// Do stuff witt the successful result

If you don't want that behavior, consider writing it this way:

 axios.get(url)
         .then(function(response) {
           // Do stuff with the successful result
         },
         function (error) {
         // any error from the get() will show up here
           Alert.alert(
             'Download failed',
             'Unable to download data from '+url+"\nError:"+error,
             [{text: 'OK'}])
         });)
    })
    .catch(function(error) {
         // any error from "doing stuff" will show up here
         console.error(error);
    })

The .then() method allows two functions, one for success the other for failure -- failure of the original promise that is, not of the success function.

Since your own code is itself failing for some reason, you certainly don't want to silence that.

like image 132
Michael Lorton Avatar answered Nov 16 '22 02:11

Michael Lorton


As Malvolio mentioned, it is the expected behavior when defining a catch method on a Promise, everything that throws in it will get catch'ed.

To me though, the best way to handle this kind of behavior would be to use Redux, since it would separate concerns between your component(s) and the data they require. Even if I don't really know your knowledge in the React ecosystem and you said it was only a prototype, I feel it would be best to use this paradigm, the sooner the better. Here are the motivations behind Redux, worth a read.

To get started, you'll have to create a redux store, which can be composed by multiple reducers that will represent small independent partitions of your state, to allow for very controlled access.

You towns reducer could look like this, and would also allow you to get loading and error status:

import { handleActions } from 'redux-actions'

const initialState = {
  data: [],
  isLoading: false,
  err: null
}

export default handleActions({

  FETCHING_TOWNS: state => ({ ...state, isLoading: true })
  FETCH_TOWNS_SUCCESS: (state, { payload }) => ({ data: payload, isLoading: false, err: null })
  FETCH_TOWNS_ERROR: (state, { payload }) => ({ data: [], err: payload, isLoading: false })

}, initialState)

And here's an example of what a store creation could be, using combineReducers:

import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'

import towns from './towns'

const reducers = combineReducers({
  towns
})

const store = createStore(reducers, applyMiddleware(thunk))

export default store

To interface your components and the reducers, you'll have to create thunk actions (thus the middleware in the store). Just be aware this is just one way to handle side effects using redux! There is multiple way to deal with this out there, but this is one of the most easiest and common to begin with.

import axios from 'axios'
import { createAction } from 'redux-actions'

const fetchingTowns = createAction('FETCHING_TOWNS')
const fetchTownsError = createAction('FETCH_TOWNS_ERROR')
const fetchTownsSuccess = createAction('FETCH_TOWNS_SUCCESS')

export const fetchTowns = () => dispatch => {
  dispatch(fetchingTowns())

  axios.get()
    .then(response => dispatch(fetchTownsSuccess(response)))
    .catch(error => {
       dispatch(fetchTownsError(err))
       Alert.alert(
         'Download failed',
         `Unable to download data from ${url}\nError: ${error}`,
         [{text: 'OK'}]
       )
     });
}

To make your components "connected" to your store, meaning they will render again once one of their props changes, you'll have to add the react-redux Provider component at the top level of your app, and then you could decorate your components, taking only data the component depends on.

Your component would then look like this, and if an error were to occur in the rendering of the child components, it won't get intercepted by the axios Promise, since it has been separated from your Component.

import React from 'react'
import { View } from 'react-native'
import { connect } from 'react-redux'

@connect(state => ({
  towns: state.towns.data
}), {
  fetchTowns
})
class TownsList extends Component {

  componentWillMount () {
    this.props.fetchTowns()
  }

  render () {
    const { towns } = this.props

    return (
      <View>
       {towns.map(town => (
         <TownComponent town={town} key={town.id}>
       )}
      </View>
    )
  }

}

I understand this can be a bit obnoxious if you're not familiar with all the new dependencies and the bit of config it adds, but I can assure you it's worth on the long run!

like image 4
Preview Avatar answered Nov 16 '22 03:11

Preview