Using Redux and Sagas architectures in large-scale state and event management applications is very common. With that, we add new layers to the application where we should be very careful about JavaScript when it comes to exception handling in our code.

If you don’t want to follow the examples step by step, you can get the source code here.

Why should you care?

With the great flexibility of JavaScript and its wide variety of paradigms it is very easy to get lost, leading to a lack of inconsistency in the development of large scale applications.

It is also very common, even if you are doing TDD (Test Driven Development) your application is not 100% error free. Mistakes can come from hundreds of different scenarios and, as a good human being, we are not exempt when it comes to covering all these gaps.

So come on!

Setting up the project

First of all, I will assume that you already have NodeJS and Yarn installed on your machine. I will also assume that you already have an account created on Sentry and know your DSN. Having that, let's make use of create-react-app which can be installed globally like this:

yarn global add create-react-app

And now we can create our app:

yarn create react-app redux-saga-sentry
cd redux-saga-sentry

And now let's install our dependencies:

yarn add redux react-redux redux-saga @sentry/browser

With that we already have all the necessary dependencies to build our app. Now as a last step, let's create a folder to store all our Redux logic and configuration:

mkdir src/store

And now we can continue to the interesting part.

Redux middleware

Redux middleware solves problems different from Express or Koa, but in a conceptually similar way. It provides a third party extension point between the submission of an action and the moment it reaches the reducer. People use Redux middleware for logging, crash reporting, chatting with an asynchronous API, routing, and more.

In our case, we will use middleware to configure our sagas, making redux-saga pay attention to all the actions that we will dispatch in our app.

So inside our store folder we will create our index.js file as follows:

import { createStore } from 'redux'

import reducers from './rootReducer'

export default function Store(initialState = null) {
  const store = createStore(
    reducers,
    initialState,
  )

  return store
}

That way we can just import our file and inject our store into the Redux provider inside our component, which we'll do next. Inside the src folder we will open the index.js file as well. We will leave it this way:

import React from 'react'
import ReactDOM from 'react-dom'
import {Provider} from 'react-redux'

import store from './store'
import App from './App'
import './index.css'

ReactDOM.render (
  <Provider store={store()}>
    <App />
  </Provider>
  , document.getElementById('root')
)

Thus ending our configuration of Redux.

Configuring Saga Middleware

In this part we need to pay close attention, as this is where we configure Saga middleware to capture the exceptions. The documentation goes through this briefly and does not provide actual usage examples.

Let's go back to our index.js file in the store folder and let's leave it like this:

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import sagas from './rootSaga'
import reducers from './rootReducer'

const sagaMiddleware = createSagaMiddleware({
  onError(err) {
    // treat the errors of the sagas here
  },
})

export default function Store(initialState = null) {
  const store = createStore(
    reducers,
    initialState,
    applyMiddleware(sagaMiddleware),
  )

  sagaMiddleware.run(sagas)

  return store
}

Take a good look at the onError function within createSagaMiddleware. This is what saga middleware calls when an error occurs within our saga.

Configuring Sentry to Collect Errors

Now that we have our saga set up, we can now integrate Sentry very simply to collect these errors. What will be covered here is a pretty simple integration, but you can take a look at the Sentry documentation to do much more advanced things like sending source maps, creating breadcrumbs, etc.

So once again, now let's modify our file to look like this:

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import * as Sentry from '@sentry/browser'

import sagas from './rootSaga'
import reducers from './rootReducer'

Sentry.init({dsn: 'YOUR_DSN_DO_SENTRY'})

const sagaMiddleware = createSagaMiddleware({
  onError(err) {
    Sentry.captureException(err)
  },
})

export default function Store(initialState = null) {
  const store = createStore(
    reducers,
    initialState,
    applyMiddleware(sagaMiddleware),
  )

  sagaMiddleware.run(sagas)

  return store
}

Notice that within our onError function we pass the error received by our callback to Sentry and it takes care of everything else.

The try/catch problem in Sagas

There is a catch in all this. 99% of the time we write a saga file, we always use a pattern that makes use of try/catch. To illustrate this, let's create a saga that will listen for an authentication action and will try to authenticate the user. Let's also simulate an error for our test.

import { takeLatest, put } from 'redux-saga/effects'

function* authenticate() {
  try {
    throw new Error('Authentication Error')
  } catch(error) {
    yield put({type: 'AUTH_FAILURE', error})
  }
}

export default function* () {
  yield takeLatest('AUTH', authenticate)
}

What do you think will happen inside this saga when an error is returned? Exactly! Because of try/catch it will never be passed to our middleware's onError function, which makes our Sentry integration quite flawed.

So, to correct this, I create a saga just to pay attention to the generic error events that I am going to emit within the catch block of my sagas. This saga will receive this error and will throw the error again, but this time out of any try/catch blocks.

Now our middleware will be able to catch this error and pass it to our onError function, which will eventually pass the error information to Sentry. And no, throwing back the error out of the try/catch block won't break our application, as our middleware is smart enough to handle it.

Here the final file:

import { takeLatest, takeEvery, put } from 'redux-saga/effects'

function * authenticate() {
  try {
    throw new Error('Authentication Error!')
  } catch(error) {
    yield put({type: 'AUTH_FAILURE', error})
    yield put({type: 'ERROR', error})
  }
}

function bubbleErrors(error) {
  if (process.env.NODE_ENV === 'development') {
    console.error(error)
  }

  throw error
}

export default function* () {
  yield takeLatest('AUTH', authenticate)
  yield takeEvery('ERROR', bubbleErrors)
}

To get more organized, let's break this into three different sagas files.

Let's create our saga to catch all errors:

import { takeEvery } from 'redux-saga/effects'

function bubbleErrors(error) {
  if (process.env.NODE_ENV === 'development') {
    console.error(error)
  }

  throw error
}

export default function* () {
  yield takeEvery('ERROR', bubbleErrors)
}

Our authentication saga that will simulate the error:

import { takeLatest, put } from 'redux-saga/effects'

function* authenticate() {
  try {
    throw new Error('Authentication Error!')
  } catch(error) {
    yield put({type: 'AUTH_FAILURE', error})
    yield put({type: 'ERROR', error})
  }
}

export default function* () {
  yield takeLatest('AUTH', authenticate)
}

And lastly, our root saga that will put it all together:

import { all, fork } from 'redux-saga/effects'

import authSaga from './sagas/auth'
import errorSaga from './sagas/error'

export default function* rootSaga() {
  yield all ([
    fork(authSaga),
    fork(errorSaga),
  ])
}

Thus ending our implementation.

Summing up

We should always pay special attention to handling and collecting errors in JavaScript. It is very easy for any mistake to break the flow of your application, and as they are front end applications, it is more costly for you to find out where the errors came from, which browser, which operating system, version… than you spend the time to beware.

If you have a suggestion or a better solution, let me know down bellow!


If you want to learn more about this I recommend the following links:

Also, as I said up there, you can get the full code of this article here.