├── .gitignore
├── src
├── actions
│ ├── api
│ │ ├── index.js
│ │ └── api.js
│ └── throw-error
│ │ ├── index.js
│ │ └── throw-error.js
├── reducers
│ ├── root
│ │ ├── index.js
│ │ └── root.js
│ └── throw-error
│ │ ├── index.js
│ │ └── throw-error.js
├── sagas
│ ├── root
│ │ ├── index.js
│ │ └── root.js
│ ├── api
│ │ ├── index.js
│ │ └── api.js
│ ├── throw-error
│ │ ├── index.js
│ │ └── throw-error.js
│ └── error-handler
│ │ ├── index.js
│ │ └── error-handler.js
├── utils
│ ├── fetch
│ │ ├── index.js
│ │ └── fetch.js
│ ├── httpbin
│ │ ├── index.js
│ │ └── httpbin.js
│ ├── logger
│ │ ├── index.js
│ │ └── logger.js
│ ├── onerror
│ │ ├── index.js
│ │ └── onerror.js
│ ├── throwing-module
│ │ ├── index.js
│ │ └── throwing-module.js
│ ├── map-state-to-props
│ │ ├── index.js
│ │ └── map-state-to-props.js
│ └── bind-action-creators
│ │ ├── index.js
│ │ └── bind-action-creators.js
├── containers
│ └── app
│ │ ├── index.js
│ │ └── app.js
├── constants
│ └── action-types
│ │ ├── index.js
│ │ └── action-types.js
├── components
│ ├── react-poop
│ │ ├── index.js
│ │ └── react-poop.js
│ └── react-component-errors
│ │ ├── index.js
│ │ └── react-component-errors.js
├── store
│ ├── index.js
│ └── configure-store.js
├── templates
│ └── index.ejs
└── index.js
├── .babelrc
├── webpack
├── production.config.js
└── development.config.js
├── .eslintrc
├── test
├── components
│ ├── react-poop
│ │ └── react-poop.spec.js
│ └── react-component-errors
│ │ └── react-component-errors.spec.js
├── sagas
│ ├── error-handler
│ │ └── error-handler.spec.js
│ └── api
│ │ └── api.spec.js
├── utils
│ ├── map-state-to-props
│ │ └── map-state-to-props.spec.js
│ └── bind-action-creators
│ │ └── bind-action-creators.spec.js
├── actions
│ ├── api
│ │ └── api.spec.js
│ └── throw-error
│ │ └── throw-error.spec.js
├── reducers
│ └── throw-error
│ │ └── throw-error.spec.js
└── containers
│ └── app
│ └── app.spec.js
├── README.md
├── LICENSE
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | node_modules
3 |
--------------------------------------------------------------------------------
/src/actions/api/index.js:
--------------------------------------------------------------------------------
1 | export * from './api'
2 |
--------------------------------------------------------------------------------
/src/reducers/root/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './root'
2 |
--------------------------------------------------------------------------------
/src/sagas/root/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './root'
2 |
--------------------------------------------------------------------------------
/src/utils/fetch/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './fetch'
2 |
--------------------------------------------------------------------------------
/src/actions/throw-error/index.js:
--------------------------------------------------------------------------------
1 | export * from './throw-error'
2 |
--------------------------------------------------------------------------------
/src/containers/app/index.js:
--------------------------------------------------------------------------------
1 | export { default, App } from './app'
2 |
--------------------------------------------------------------------------------
/src/utils/httpbin/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './httpbin'
2 |
--------------------------------------------------------------------------------
/src/utils/logger/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './logger'
2 |
--------------------------------------------------------------------------------
/src/utils/onerror/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './onerror'
2 |
--------------------------------------------------------------------------------
/src/constants/action-types/index.js:
--------------------------------------------------------------------------------
1 | export * from './action-types'
2 |
--------------------------------------------------------------------------------
/src/sagas/api/index.js:
--------------------------------------------------------------------------------
1 | export { default, httpbinGet } from './api'
2 |
--------------------------------------------------------------------------------
/src/components/react-poop/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './react-poop'
2 |
--------------------------------------------------------------------------------
/src/sagas/throw-error/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './throw-error'
2 |
--------------------------------------------------------------------------------
/src/utils/throwing-module/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './throwing-module'
2 |
--------------------------------------------------------------------------------
/src/utils/throwing-module/throwing-module.js:
--------------------------------------------------------------------------------
1 | throw new Error('Module error')
2 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | export { default as configureStore } from './configure-store'
2 |
--------------------------------------------------------------------------------
/src/utils/map-state-to-props/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './map-state-to-props'
2 |
--------------------------------------------------------------------------------
/src/sagas/error-handler/index.js:
--------------------------------------------------------------------------------
1 | export { default, errorHandler } from './error-handler'
2 |
--------------------------------------------------------------------------------
/src/utils/bind-action-creators/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './bind-action-creators'
2 |
--------------------------------------------------------------------------------
/src/components/react-component-errors/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './react-component-errors'
2 |
3 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 |
4 | "plugins": ["transform-object-rest-spread"]
5 | }
6 |
--------------------------------------------------------------------------------
/src/reducers/throw-error/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './throw-error'
2 | export * from './throw-error'
3 |
--------------------------------------------------------------------------------
/src/reducers/root/root.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import throwError from 'reducers/throw-error'
3 |
4 | export default combineReducers({ throwError })
5 |
--------------------------------------------------------------------------------
/src/utils/onerror/onerror.js:
--------------------------------------------------------------------------------
1 | import Logger from 'utils/logger'
2 |
3 | window.onerror = (message, source, lineno, colno, error) => {
4 | Logger.error(error, { message, source, lineno, colno })
5 |
6 | return true
7 | }
8 |
--------------------------------------------------------------------------------
/src/templates/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Proper Error Handling
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/utils/map-state-to-props/map-state-to-props.js:
--------------------------------------------------------------------------------
1 | export default (func, errorHandler) => (state, ownProps) => {
2 | try {
3 | return func(state, ownProps)
4 | } catch (error) {
5 | errorHandler(error, { state, ownProps })
6 | }
7 |
8 | return {}
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/fetch/fetch.js:
--------------------------------------------------------------------------------
1 | import fetch from 'isomorphic-fetch'
2 |
3 | export default url => fetch(url)
4 | .then(response => {
5 | if (!response.ok) {
6 | return Promise.reject(response.statusText)
7 | }
8 |
9 | return { response }
10 | })
11 | .catch(error => ({ error }))
12 |
--------------------------------------------------------------------------------
/src/utils/httpbin/httpbin.js:
--------------------------------------------------------------------------------
1 | import fetch from 'utils/fetch'
2 |
3 | export default {
4 |
5 | get(options) {
6 | let url = 'https://httpbin.org/'
7 |
8 | if (options && options.status) {
9 | url += `status/${options.status}`
10 | }
11 |
12 | return fetch(url)
13 | },
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/sagas/root/root.js:
--------------------------------------------------------------------------------
1 | import { fork } from 'redux-saga/effects'
2 | import api from 'sagas/api'
3 | import throwError from 'sagas/throw-error'
4 | import errorHandler from 'sagas/error-handler'
5 |
6 | export default function* root() {
7 | yield [
8 | fork(api),
9 | fork(throwError),
10 | fork(errorHandler),
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/src/actions/api/api.js:
--------------------------------------------------------------------------------
1 | import { API_GET, API_GET_SUCCESS, API_GET_FAILURE } from 'constants/action-types'
2 |
3 | const get = options => ({ type: API_GET, options })
4 | export const getSuccess = response => ({ type: API_GET_SUCCESS, response })
5 | export const getFailure = error => ({ type: API_GET_FAILURE, error })
6 |
7 | export const getStatus = status => get({ status })
8 |
--------------------------------------------------------------------------------
/src/components/react-poop/react-poop.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import poop from 'react-poop'
3 |
4 | const ReactPoop = ({ componentsShouldThrow }) => (
5 | ReactPoop{componentsShouldThrow && this.does.not.exist}
6 | )
7 |
8 | ReactPoop.propTypes = {
9 | componentsShouldThrow: PropTypes.bool,
10 | }
11 |
12 | export default poop(ReactPoop)
13 |
--------------------------------------------------------------------------------
/src/sagas/error-handler/error-handler.js:
--------------------------------------------------------------------------------
1 | import { takeEvery } from 'redux-saga'
2 | import { call } from 'redux-saga/effects'
3 | import Logger from 'utils/logger'
4 |
5 | export function* errorHandler({ error, type }) {
6 | yield call(Logger.info, error, type)
7 | }
8 |
9 | function* takeEveryError() {
10 | yield* takeEvery(action => action.error, errorHandler)
11 | }
12 |
13 | export default takeEveryError
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { Provider } from 'react-redux'
4 | import App from 'containers/app'
5 | import { configureStore } from 'store'
6 | // import ThrowingModule from 'utils/throwing-module'
7 |
8 | const store = configureStore()
9 |
10 | ReactDOM.render(
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | )
16 |
--------------------------------------------------------------------------------
/src/constants/action-types/action-types.js:
--------------------------------------------------------------------------------
1 | export const THROW_ERROR_IN_COMPONENTS = 'THROW_ERROR_IN_COMPONENTS'
2 | export const THROW_ERROR_IN_REDUCERS = 'THROW_ERROR_IN_REDUCERS'
3 | export const THROW_ERROR_IN_SAGAS = 'THROW_ERROR_IN_SAGAS'
4 | export const THROW_ERROR_IN_SELECTORS = 'THROW_ERROR_IN_SELECTORS'
5 |
6 | export const API_GET = 'API_GET'
7 | export const API_GET_SUCCESS = 'API_GET_SUCCESS'
8 | export const API_GET_FAILURE = 'API_GET_FAILURE'
9 |
--------------------------------------------------------------------------------
/webpack/production.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const development = require('./development.config.js')
3 |
4 | development.plugins.push(
5 | new webpack.optimize.UglifyJsPlugin(),
6 | new webpack.optimize.OccurrenceOrderPlugin(),
7 | new webpack.optimize.DedupePlugin(),
8 | new webpack.DefinePlugin({
9 | 'process.env': {
10 | NODE_ENV: JSON.stringify('production'),
11 | },
12 | })
13 | )
14 |
15 | module.exports = development
16 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 |
4 | "env": {
5 | "mocha": true
6 | },
7 |
8 | "rules": {
9 | "semi": [2, "never"],
10 | "no-console": 0
11 | },
12 |
13 | "parserOptions": {
14 | "ecmaFeatures": {
15 | "experimentalObjectRestSpread": true
16 | }
17 | },
18 |
19 | "settings": {
20 | "import/resolver": {
21 | "webpack": {
22 | "config": "webpack/development.config.js"
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/test/components/react-poop/react-poop.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import { shallow } from 'enzyme'
3 | import React from 'react'
4 | import ReactPoop from 'components/react-poop'
5 |
6 | describe('', () => {
7 | it('renders a div', () => {
8 | const wrapper = shallow()
9 |
10 | assert(wrapper.equals())
11 | })
12 |
13 | it('renders a 💩', () => {
14 | const wrapper = shallow()
15 |
16 | assert(wrapper.contains('💩'))
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/src/actions/throw-error/throw-error.js:
--------------------------------------------------------------------------------
1 | import {
2 | THROW_ERROR_IN_COMPONENTS,
3 | THROW_ERROR_IN_REDUCERS,
4 | THROW_ERROR_IN_SAGAS,
5 | THROW_ERROR_IN_SELECTORS,
6 | } from 'constants/action-types'
7 |
8 | export const inComponents = () => ({ type: THROW_ERROR_IN_COMPONENTS })
9 | export const inActions = () => { throw new Error('Action error') }
10 | export const inReducers = () => ({ type: THROW_ERROR_IN_REDUCERS })
11 | export const inSagas = () => ({ type: THROW_ERROR_IN_SAGAS })
12 | export const inSelectors = () => ({ type: THROW_ERROR_IN_SELECTORS })
13 |
--------------------------------------------------------------------------------
/test/sagas/error-handler/error-handler.spec.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import assert from 'assert'
3 | import { call } from 'redux-saga/effects'
4 | import * as ApiActions from 'actions/api'
5 | import Logger from 'utils/logger'
6 | import { errorHandler } from 'sagas/error-handler'
7 |
8 | describe('errorHandler', () => {
9 | it('fires the logging', () => {
10 | const action = ApiActions.getFailure('error')
11 | const generator = errorHandler(action)
12 |
13 | assert.deepEqual(generator.next().value, call(Logger.info, action.error, action.type))
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/sagas/throw-error/throw-error.js:
--------------------------------------------------------------------------------
1 | import { takeEvery } from 'redux-saga'
2 | import { call } from 'redux-saga/effects'
3 | import { THROW_ERROR_IN_SAGAS } from 'constants/action-types'
4 | import Logger from 'utils/logger'
5 |
6 | function* throwError() {
7 | try {
8 | yield call(() => { throw new Error('Saga error') })
9 | } catch (error) {
10 | yield call(Logger.error, error, { type: THROW_ERROR_IN_SAGAS })
11 | }
12 | }
13 |
14 | function* takeEveryThrowErrorInSagas() {
15 | yield* takeEvery(THROW_ERROR_IN_SAGAS, throwError)
16 | }
17 |
18 | export default takeEveryThrowErrorInSagas
19 |
--------------------------------------------------------------------------------
/src/sagas/api/api.js:
--------------------------------------------------------------------------------
1 | import { takeEvery } from 'redux-saga'
2 | import { call, put } from 'redux-saga/effects'
3 | import * as ApiActions from 'actions/api'
4 | import httpbin from 'utils/httpbin'
5 | import { API_GET } from 'constants/action-types'
6 |
7 | export function* httpbinGet({ options }) {
8 | const { response, error } = yield call(httpbin.get, options)
9 | if (response) {
10 | yield put(ApiActions.getSuccess())
11 | } else {
12 | yield put(ApiActions.getFailure(error))
13 | }
14 | }
15 |
16 | function* takeEveryApiGet() {
17 | yield* takeEvery(API_GET, httpbinGet)
18 | }
19 |
20 | export default takeEveryApiGet
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Proper Error Handling
2 |
3 | No matter how good you are as a developer and how many tests you write: your application will throw errors.
4 | At YPlan we tried to catch all the exceptions and handle them in the right way to give our users the best possible experience.
5 | In this talk, I will go through the problems we tried to solve and the solutions we implemented #forreal
6 |
7 | [https://speakerdeck.com/michelebertoli/proper-error-handling](https://speakerdeck.com/michelebertoli/proper-error-handling)
8 |
9 | ## Usage
10 |
11 | ```bash
12 | npm install
13 | npm start
14 | ```
15 |
16 | ## Test
17 |
18 | ```bash
19 | npm test
20 | ```
21 |
--------------------------------------------------------------------------------
/src/utils/logger/logger.js:
--------------------------------------------------------------------------------
1 | import StackTrace from 'stacktrace-js'
2 |
3 | const log = stack => {
4 | console.log(stack.map(frame => frame.toString()).join('\n'))
5 | console.groupEnd()
6 | }
7 |
8 | export default {
9 |
10 | error(error, extra) {
11 | if (console.group) {
12 | console.group('💀')
13 | console.log(error)
14 | console.log(extra)
15 |
16 | StackTrace.fromError(error).then(log)
17 | }
18 | },
19 |
20 | info(message, extra) {
21 | if (console.group) {
22 | console.group('ℹ️')
23 | console.log(message)
24 | console.log(extra)
25 | console.groupEnd()
26 | }
27 | },
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/test/components/react-component-errors/react-component-errors.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import { shallow } from 'enzyme'
3 | import React from 'react'
4 | import ReactComponentErrors from 'components/react-component-errors'
5 |
6 | describe('', () => {
7 | it('renders a div', () => {
8 | const wrapper = shallow()
9 |
10 | assert(wrapper.equals())
11 | })
12 |
13 | it('throws an error', () => {
14 | const wrapper = shallow()
15 |
16 | assert.throws(
17 | () => wrapper.instance().throwError({ componentsShouldThrow: true }),
18 | /Component error/
19 | )
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/webpack/development.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const HtmlWebpackPlugin = require('html-webpack-plugin')
3 |
4 | module.exports = {
5 |
6 | devtool: 'source-map', // 'eval',
7 |
8 | entry: [
9 | './src/utils/onerror',
10 | './src',
11 | ],
12 |
13 | output: {
14 | path: 'build',
15 | filename: 'bundle.js',
16 | },
17 |
18 | resolve: {
19 | root: [
20 | path.resolve('./src'),
21 | ],
22 | },
23 |
24 | module: {
25 | loaders: [
26 | {
27 | test: /\.js$/,
28 | exclude: /(node_modules|bower_components)/,
29 | loader: 'babel',
30 | },
31 | ],
32 | },
33 |
34 | plugins: [
35 | new HtmlWebpackPlugin({
36 | template: './src/templates/index.ejs',
37 | }),
38 | ],
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/test/utils/map-state-to-props/map-state-to-props.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import sinon from 'sinon'
3 | import safeMapStateToProps from 'utils/map-state-to-props'
4 |
5 | describe('safeMapStateToProps', () => {
6 | let errorHandler
7 |
8 | beforeEach(() => {
9 | errorHandler = sinon.spy()
10 | })
11 |
12 | it('catches the error', () => {
13 | const error = new Error('test')
14 | const func = () => { throw error }
15 | const state = { state: 'state' }
16 | const ownProps = { ownProps: 'ownProps' }
17 | const mapStateToProps = safeMapStateToProps(func, errorHandler)
18 | mapStateToProps(state, ownProps)
19 |
20 | assert.deepEqual(errorHandler.args[0][0], error)
21 | assert.deepEqual(errorHandler.args[0][1], { state, ownProps })
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/src/store/configure-store.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import { createStore, applyMiddleware } from 'redux'
3 | import reduxCatch from 'redux-catch'
4 | import createSagaMiddleware from 'redux-saga'
5 | import createLogger from 'redux-logger'
6 | import rootReducer from 'reducers/root'
7 | import rootSaga from 'sagas/root'
8 | import Logger from 'utils/logger'
9 |
10 | const errorHandler = (error, getState) => Logger.error(error, getState())
11 |
12 | const sagaMiddleware = createSagaMiddleware()
13 |
14 | export default () => {
15 | const store = createStore(
16 | rootReducer,
17 | applyMiddleware(
18 | reduxCatch(errorHandler),
19 | sagaMiddleware,
20 | createLogger({ collapsed: true })
21 | )
22 | )
23 |
24 | sagaMiddleware.run(rootSaga)
25 |
26 | return store
27 | }
28 |
--------------------------------------------------------------------------------
/src/reducers/throw-error/throw-error.js:
--------------------------------------------------------------------------------
1 | import {
2 | THROW_ERROR_IN_COMPONENTS,
3 | THROW_ERROR_IN_REDUCERS,
4 | THROW_ERROR_IN_SELECTORS,
5 | } from 'constants/action-types'
6 |
7 | export default (state = {}, action) => {
8 | switch (action.type) {
9 | case THROW_ERROR_IN_COMPONENTS:
10 | return { componentsShouldThrow: true }
11 |
12 | case THROW_ERROR_IN_REDUCERS:
13 | throw new Error('Reducer error')
14 |
15 | case THROW_ERROR_IN_SELECTORS:
16 | return { selectorsShouldThrow: true }
17 |
18 | default:
19 | return state
20 | }
21 | }
22 |
23 | export const getComponentsShouldThrow = state => state.throwError.componentsShouldThrow
24 |
25 | export const getSelectorsShouldThrow = state => {
26 | if (state.throwError.selectorsShouldThrow) {
27 | throw new Error('Selector error')
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/utils/bind-action-creators/bind-action-creators.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators as originalBindActionCreators } from 'redux'
2 |
3 | const wrap = (func, errorHandler, action) => (
4 | (...args) => {
5 | try {
6 | return func.apply(null, args)
7 | } catch (error) {
8 | return errorHandler(error, { action })
9 | }
10 | }
11 | )
12 |
13 | export default (actionCreators, dispatch, errorHandler) => {
14 | let boundActionCreators = originalBindActionCreators(actionCreators, dispatch)
15 |
16 | if (typeof boundActionCreators === 'function') {
17 | boundActionCreators = wrap(boundActionCreators, errorHandler, actionCreators.name)
18 | } else {
19 | Object.keys(boundActionCreators).forEach(key => {
20 | boundActionCreators[key] = wrap(boundActionCreators[key], errorHandler, key)
21 | })
22 | }
23 |
24 | return boundActionCreators
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/react-component-errors/react-component-errors.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import wrapReactLifecycleMethodsWithTryCatch, { config } from 'react-component-errors'
3 | import Logger from 'utils/logger'
4 |
5 | config.errorHandler = errorReport => Logger.error(errorReport.error, errorReport)
6 |
7 | class ReactComponentErrors extends Component {
8 |
9 | componentWillReceiveProps(nextProps) {
10 | this.throwError(nextProps)
11 | }
12 |
13 | throwError(props) {
14 | if (props.componentsShouldThrow) {
15 | throw new Error('Component error')
16 | }
17 | }
18 |
19 | render() {
20 | return ReactComponentErrors
21 | }
22 |
23 | }
24 |
25 | ReactComponentErrors.propTypes = {
26 | componentsShouldThrow: PropTypes.bool,
27 | }
28 |
29 | wrapReactLifecycleMethodsWithTryCatch(ReactComponentErrors)
30 |
31 | export default ReactComponentErrors
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Michele Bertoli
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/sagas/api/api.spec.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import assert from 'assert'
3 | import { call, put } from 'redux-saga/effects'
4 | import * as ApiActions from 'actions/api'
5 | import httpbin from 'utils/httpbin'
6 | import { httpbinGet } from 'sagas/api'
7 |
8 | describe('httpbinGet', () => {
9 | const options = { options: 'options' }
10 |
11 | it('fires the http call', () => {
12 | const generator = httpbinGet({ options })
13 |
14 | assert.deepEqual(generator.next().value, call(httpbin.get, options))
15 | })
16 |
17 | it('puts the success action', () => {
18 | const generator = httpbinGet({ options })
19 |
20 | assert.deepEqual(generator.next().value, call(httpbin.get, options))
21 | assert.deepEqual(generator.next({ response: 'response' }).value, put(ApiActions.getSuccess()))
22 | })
23 |
24 | it('puts the fail action', () => {
25 | const generator = httpbinGet({ options })
26 | const error = { error: 'error' }
27 |
28 | assert.deepEqual(generator.next().value, call(httpbin.get, options))
29 | assert.deepEqual(generator.next({ error }).value, put(ApiActions.getFailure(error)))
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/test/actions/api/api.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import { API_GET, API_GET_SUCCESS, API_GET_FAILURE } from 'constants/action-types'
3 | import * as ApiActions from 'actions/api'
4 |
5 | describe('api', () => {
6 | describe('getSuccess', () => {
7 | it('creates a get success action', () => {
8 | const response = 'response'
9 | const expected = {
10 | type: API_GET_SUCCESS,
11 | response,
12 | }
13 |
14 | assert.deepEqual(ApiActions.getSuccess(response), expected)
15 | })
16 | })
17 |
18 | describe('getFailure', () => {
19 | it('creates a get fail action', () => {
20 | const error = 'error'
21 | const expected = {
22 | type: API_GET_FAILURE,
23 | error,
24 | }
25 |
26 | assert.deepEqual(ApiActions.getFailure(error), expected)
27 | })
28 | })
29 |
30 | describe('getStatus', () => {
31 | it('creates a get status action', () => {
32 | const status = 'status'
33 | const expected = {
34 | type: API_GET,
35 | options: { status },
36 | }
37 |
38 | assert.deepEqual(ApiActions.getStatus(status), expected)
39 | })
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/test/utils/bind-action-creators/bind-action-creators.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import sinon from 'sinon'
3 | import safeBindActionCreators from 'utils/bind-action-creators'
4 |
5 | describe('safeBindActionCreators', () => {
6 | const error = new Error('test')
7 | const dispatch = func => func
8 | let errorHandler
9 |
10 | beforeEach(() => {
11 | errorHandler = sinon.spy()
12 | })
13 |
14 | it('wraps a single function', () => {
15 | const actionCreators = () => { throw error }
16 | const boundActionCreators = safeBindActionCreators(actionCreators, dispatch, errorHandler)
17 | boundActionCreators()
18 |
19 | assert.deepEqual(errorHandler.args[0][0], error)
20 | assert.deepEqual(errorHandler.args[0][1], { action: 'actionCreators' })
21 | })
22 |
23 | it('wraps multiples functions', () => {
24 | const actionCreators = {
25 | actionCreator: () => { throw error },
26 | }
27 | const boundActionCreators = safeBindActionCreators(actionCreators, dispatch, errorHandler)
28 | boundActionCreators.actionCreator()
29 |
30 | assert.deepEqual(errorHandler.args[0][0], error)
31 | assert.deepEqual(errorHandler.args[0][1], { action: 'actionCreator' })
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/test/actions/throw-error/throw-error.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import {
3 | THROW_ERROR_IN_COMPONENTS,
4 | THROW_ERROR_IN_REDUCERS,
5 | THROW_ERROR_IN_SAGAS,
6 | THROW_ERROR_IN_SELECTORS,
7 | } from 'constants/action-types'
8 | import * as ThrowErrorActions from 'actions/throw-error'
9 |
10 | describe('throw error', () => {
11 | describe('inComponents', () => {
12 | it('creates a throw error in components action', () => {
13 | const expected = {
14 | type: THROW_ERROR_IN_COMPONENTS,
15 | }
16 |
17 | assert.deepEqual(ThrowErrorActions.inComponents(), expected)
18 | })
19 | })
20 |
21 | describe('inActions', () => {
22 | it('throws an error', () => {
23 | assert.throws(() => ThrowErrorActions.inActions(), /Action error/)
24 | })
25 | })
26 |
27 | describe('inReducers', () => {
28 | it('creates a throw error in reducers action', () => {
29 | const expected = {
30 | type: THROW_ERROR_IN_REDUCERS,
31 | }
32 |
33 | assert.deepEqual(ThrowErrorActions.inReducers(), expected)
34 | })
35 | })
36 |
37 | describe('inSagas', () => {
38 | it('creates a throw error in sagas action', () => {
39 | const expected = {
40 | type: THROW_ERROR_IN_SAGAS,
41 | }
42 |
43 | assert.deepEqual(ThrowErrorActions.inSagas(), expected)
44 | })
45 | })
46 |
47 | describe('inSelectors', () => {
48 | it('creates a throw error in selectors action', () => {
49 | const expected = {
50 | type: THROW_ERROR_IN_SELECTORS,
51 | }
52 |
53 | assert.deepEqual(ThrowErrorActions.inSelectors(), expected)
54 | })
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/test/reducers/throw-error/throw-error.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import {
3 | THROW_ERROR_IN_COMPONENTS,
4 | THROW_ERROR_IN_REDUCERS,
5 | THROW_ERROR_IN_SELECTORS,
6 | } from 'constants/action-types'
7 | import throwError, { getComponentsShouldThrow, getSelectorsShouldThrow } from 'reducers/throw-error'
8 |
9 | describe('throw error', () => {
10 | it('returns the initial state', () => {
11 | assert.deepEqual(throwError(undefined, {}), {})
12 | })
13 |
14 | it('handles THROW_ERROR_IN_COMPONENTS', () => {
15 | const action = {
16 | type: THROW_ERROR_IN_COMPONENTS,
17 | }
18 | const expected = { componentsShouldThrow: true }
19 |
20 | assert.deepEqual(throwError(undefined, action), expected)
21 | })
22 |
23 | it('throws an error', () => {
24 | const action = {
25 | type: THROW_ERROR_IN_REDUCERS,
26 | }
27 |
28 | assert.throws(() => throwError(undefined, action), /Reducer error/)
29 | })
30 |
31 | it('handles THROW_ERROR_IN_SELECTORS', () => {
32 | const action = {
33 | type: THROW_ERROR_IN_SELECTORS,
34 | }
35 | const expected = { selectorsShouldThrow: true }
36 |
37 | assert.deepEqual(throwError(undefined, action), expected)
38 | })
39 | })
40 |
41 | describe('getComponentsShouldThrow', () => {
42 | it('returns componentsShouldThrow', () => {
43 | const state = {
44 | throwError: { componentsShouldThrow: true },
45 | }
46 |
47 | assert.equal(getComponentsShouldThrow(state), true)
48 | })
49 | })
50 |
51 | describe('getSelectorsShouldThrow', () => {
52 | it('throws an error', () => {
53 | const state = {
54 | throwError: { selectorsShouldThrow: true },
55 | }
56 |
57 | assert.throws(() => getSelectorsShouldThrow(state), /Selector error/)
58 | })
59 | })
60 |
--------------------------------------------------------------------------------
/src/containers/app/app.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import { connect } from 'react-redux'
3 | import * as ThrowErrorActions from 'actions/throw-error'
4 | import { getComponentsShouldThrow, getSelectorsShouldThrow } from 'reducers/throw-error'
5 | import * as ApiActions from 'actions/api'
6 | import ReactComponentErrors from 'components/react-component-errors'
7 | import ReactPoop from 'components/react-poop'
8 | import Logger from 'utils/logger'
9 | import safeBindActionCreators from 'utils/bind-action-creators'
10 | import safeMapStateToProps from 'utils/map-state-to-props'
11 |
12 | export const App = props => (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | )
29 |
30 | App.propTypes = {
31 | apiActions: PropTypes.object,
32 | componentsShouldThrow: PropTypes.bool,
33 | throwErrorActions: PropTypes.object,
34 | }
35 |
36 | const mapStateToProps = safeMapStateToProps(state => ({
37 | componentsShouldThrow: getComponentsShouldThrow(state),
38 | selectorsShouldThrow: getSelectorsShouldThrow(state),
39 | }), Logger.error)
40 |
41 | const mapDispatchToProps = dispatch => ({
42 | apiActions: safeBindActionCreators(ApiActions, dispatch, Logger.error),
43 | throwErrorActions: safeBindActionCreators(ThrowErrorActions, dispatch, Logger.error),
44 | })
45 |
46 | export default connect(mapStateToProps, mapDispatchToProps)(App)
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-error-handling",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "webpack -p --config webpack/production.config.js",
8 | "lint": "eslint ./src",
9 | "start": "webpack-dev-server --config webpack/development.config.js --content-base build",
10 | "test": "NODE_PATH=./src mocha --compilers js:babel-register --reporter emoji-reporter ./test/**/*.spec.js",
11 | "test:watch": "npm test -- --watch"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+ssh://git@gitlab.com/MicheleBertoli/react-redux-error-handling.git"
16 | },
17 | "author": "",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://gitlab.com/MicheleBertoli/react-redux-error-handling/issues"
21 | },
22 | "homepage": "https://gitlab.com/MicheleBertoli/react-redux-error-handling#README",
23 | "dependencies": {
24 | "babel-polyfill": "^6.13.0",
25 | "isomorphic-fetch": "^2.2.1",
26 | "raven-js": "^3.6.1",
27 | "react": "^15.3.1",
28 | "react-component-errors": "0.0.6",
29 | "react-dom": "^15.3.1",
30 | "react-poop": "^0.2.0",
31 | "react-redux": "^4.4.5",
32 | "redux": "^3.6.0",
33 | "redux-catch": "^1.2.0",
34 | "redux-logger": "^2.6.1",
35 | "redux-saga": "^0.11.1",
36 | "stacktrace-js": "^1.3.1"
37 | },
38 | "devDependencies": {
39 | "babel-core": "^6.14.0",
40 | "babel-loader": "^6.2.5",
41 | "babel-plugin-transform-object-rest-spread": "^6.8.0",
42 | "babel-preset-es2015": "^6.14.0",
43 | "babel-preset-react": "^6.11.1",
44 | "babel-register": "^6.14.0",
45 | "emoji-reporter": "^0.3.0",
46 | "enzyme": "^2.4.1",
47 | "eslint": "^2.13.1",
48 | "eslint-config-airbnb": "^9.0.1",
49 | "eslint-import-resolver-webpack": "^0.3.1",
50 | "eslint-plugin-import": "^1.9.2",
51 | "eslint-plugin-jsx-a11y": "^1.5.3",
52 | "eslint-plugin-react": "^5.2.2",
53 | "html-webpack-plugin": "^2.22.0",
54 | "mocha": "^3.0.2",
55 | "react-addons-test-utils": "^15.3.1",
56 | "sinon": "^1.17.5",
57 | "webpack": "^1.13.2",
58 | "webpack-dev-server": "^1.15.1"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/test/containers/app/app.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import sinon from 'sinon'
3 | import { shallow } from 'enzyme'
4 | import React from 'react'
5 | import { App } from 'containers/app'
6 | import ReactPoop from 'components/react-poop'
7 |
8 | describe('', () => {
9 | let props
10 |
11 | beforeEach(() => {
12 | props = {
13 | componentsShouldThrow: true,
14 | throwErrorActions: {
15 | inComponents: sinon.spy(),
16 | inReducers: sinon.spy(),
17 | inSagas: sinon.spy(),
18 | inActions: sinon.spy(),
19 | inSelectors: sinon.spy(),
20 | },
21 | apiActions: {
22 | getStatus: sinon.spy(),
23 | },
24 | }
25 | })
26 |
27 | it('passes the componentsShouldThrow prop to ReactComponentErrors', () => {
28 | const wrapper = shallow()
29 | const element = wrapper.find('ReactComponentErrors')
30 |
31 | assert.equal(element.prop('componentsShouldThrow'), props.componentsShouldThrow)
32 | })
33 |
34 | it('passes the componentsShouldThrow prop to ReactPoop', () => {
35 | const wrapper = shallow()
36 | const element = wrapper.find(ReactPoop)
37 |
38 | assert.equal(element.prop('componentsShouldThrow'), props.componentsShouldThrow)
39 | })
40 |
41 | it('fires the inComponents action', () => {
42 | const wrapper = shallow()
43 | const element = wrapper.find('button').at(0)
44 | element.simulate('click')
45 |
46 | assert(props.throwErrorActions.inComponents.called)
47 | })
48 |
49 | it('fires the inActions action', () => {
50 | const wrapper = shallow()
51 | const element = wrapper.find('button').at(1)
52 | element.simulate('click')
53 |
54 | assert(props.throwErrorActions.inActions.called)
55 | })
56 |
57 | it('fires the inReducers action', () => {
58 | const wrapper = shallow()
59 | const element = wrapper.find('button').at(2)
60 | element.simulate('click')
61 |
62 | assert(props.throwErrorActions.inReducers.called)
63 | })
64 |
65 | it('fires the inSagas action', () => {
66 | const wrapper = shallow()
67 | const element = wrapper.find('button').at(3)
68 | element.simulate('click')
69 |
70 | assert(props.throwErrorActions.inSagas.called)
71 | })
72 |
73 | it('fires the inSelectors action', () => {
74 | const wrapper = shallow()
75 | const element = wrapper.find('button').at(4)
76 | element.simulate('click')
77 |
78 | assert(props.throwErrorActions.inSelectors.called)
79 | })
80 |
81 | it('fires the getStatus 404 action', () => {
82 | const wrapper = shallow()
83 | const element = wrapper.find('button').at(5)
84 | element.simulate('click')
85 |
86 | assert(props.apiActions.getStatus.called)
87 | assert.equal(props.apiActions.getStatus.args[0][0], 404)
88 | })
89 |
90 | it('fires the getStatus 500 action', () => {
91 | const wrapper = shallow()
92 | const element = wrapper.find('button').at(6)
93 | element.simulate('click')
94 |
95 | assert(props.apiActions.getStatus.called)
96 | assert.equal(props.apiActions.getStatus.args[0][0], 500)
97 | })
98 | })
99 |
--------------------------------------------------------------------------------