diff --git a/src/core/system.js b/src/core/system.js index 73c602a8..b7b70f19 100644 --- a/src/core/system.js +++ b/src/core/system.js @@ -176,7 +176,7 @@ export default class Store { if(!isFn(newAction)) { throw new TypeError("wrapActions needs to return a function that returns a new function (ie the wrapped action)") } - return newAction + return wrapWithTryCatch(newAction) }, action || Function.prototype) }) } @@ -256,11 +256,11 @@ export default class Store { return objMap(obj, (fn) => { return (...args) => { - let res = fn.apply(null, [getNestedState(), ...args]) + let res = wrapWithTryCatch(fn).apply(null, [getNestedState(), ...args]) // If a selector returns a function, give it the system - for advanced usage if(typeof(res) === "function") - res = res(getSystem()) + res = wrapWithTryCatch(res)(getSystem()) return res } @@ -331,7 +331,7 @@ function callAfterLoad(plugins, system, { hasLoaded } = {}) { if(isObject(plugins) && !isArray(plugins)) { if(typeof plugins.afterLoad === "function") { calledSomething = true - plugins.afterLoad.call(this, system) + wrapWithTryCatch(plugins.afterLoad).call(this, system) } } @@ -436,14 +436,36 @@ function makeReducer(reducerObj) { if(!reducerObj) return state - let redFn = reducerObj[action.type] + let redFn = (reducerObj[action.type]) if(redFn) { - return redFn(state, action) + const res = wrapWithTryCatch(redFn)(state, action) + // If the try/catch wrapper kicks in, we'll get null back... + // in that case, we want to avoid making any changes to state + return res === null ? state : res } return state } } +function wrapWithTryCatch(fn, { + logErrors = true +} = {}) { + if(typeof fn !== "function") { + return fn + } + + return function(...args) { + try { + return fn.call(this, ...args) + } catch(e) { + if(logErrors) { + console.error(e) + } + return null + } + } +} + function configureStore(rootReducer, initialState, getSystem) { const store = createStoreWithMiddleware(rootReducer, initialState, getSystem) diff --git a/test/core/system/system.js b/test/core/system/system.js index fc5db9a1..24bfbf8a 100644 --- a/test/core/system/system.js +++ b/test/core/system/system.js @@ -571,116 +571,6 @@ describe("bound system", function(){ // Then expect(renderedComponent.text()).toEqual("This came from mapStateToProps and this came from the system and this came from my own props") }) - - it("should catch errors thrown inside of React Component Class render methods", function() { - // Given - // eslint-disable-next-line react/require-render-return - class BrokenComponent extends React.Component { - render() { - throw new Error("This component is broken") - } - } - const system = new System({ - plugins: [ - ViewPlugin, - { - components: { - BrokenComponent - } - } - ] - }) - - // When - var Component = system.getSystem().getComponent("BrokenComponent") - const renderedComponent = render() - - // Then - expect(renderedComponent.text()).toEqual("😱 Could not render BrokenComponent, see the console.") - }) - - it("should catch errors thrown inside of pure component render methods", function() { - // Given - // eslint-disable-next-line react/require-render-return - class BrokenComponent extends PureComponent { - render() { - throw new Error("This component is broken") - } - } - - const system = new System({ - plugins: [ - ViewPlugin, - { - components: { - BrokenComponent - } - } - ] - }) - - // When - var Component = system.getSystem().getComponent("BrokenComponent") - const renderedComponent = render() - - // Then - expect(renderedComponent.text()).toEqual("😱 Could not render BrokenComponent, see the console.") - }) - - it("should catch errors thrown inside of stateless component functions", function() { - // Given - // eslint-disable-next-line react/require-render-return - let BrokenComponent = function BrokenComponent() { throw new Error("This component is broken") } - const system = new System({ - plugins: [ - ViewPlugin, - { - components: { - BrokenComponent - } - } - ] - }) - - // When - var Component = system.getSystem().getComponent("BrokenComponent") - const renderedComponent = render() - - // Then - expect(renderedComponent.text().startsWith("😱 Could not render")).toEqual(true) - }) - - it("should catch errors thrown inside of container components", function() { - // Given - // eslint-disable-next-line react/require-render-return - class BrokenComponent extends React.Component { - render() { - throw new Error("This component is broken") - } - } - - const system = new System({ - plugins: [ - ViewPlugin, - { - components: { - BrokenComponent - } - } - ] - }) - - // When - var Component = system.getSystem().getComponent("BrokenComponent", true) - const renderedComponent = render( - - - - ) - - // Then - expect(renderedComponent.text()).toEqual("😱 Could not render BrokenComponent, see the console.") - }) }) describe("afterLoad", function() { @@ -793,4 +683,299 @@ describe("bound system", function(){ expect(res).toEqual("so selective") }) }) + + describe("error catching", function() { + it("should encapsulate thrown errors in an afterLoad method", function() { + // Given + const ThrowyPlugin = { + afterLoad(system) { + throw new Error("afterLoad BREAKS STUFF!") + }, + statePlugins: { + doge: { + selectors: { + wow: () => (system) => { + return "so selective" + } + } + } + } + } + + const system = new System({ + plugins: [] + }) + + + // When + expect(function() { + system.register([ThrowyPlugin]) + // var resSystem = system.getSystem() + }).toNotThrow() + }) + + it("should encapsulate thrown errors in an action creator", function(){ + + // Given + const system = new System({ + plugins: { + statePlugins: { + throw: { + actions: { + func() { + throw new Error("this action creator THROWS!") + } + } + } + } + } + + }) + + expect(function() { + // TODO: fix existing action error catcher that creates THROWN ERR actions + system.getSystem().throwActions.func() + }).toNotThrow() + }) + + it("should encapsulate thrown errors in a reducer", function(){ + + // Given + const system = new System({ + plugins: { + statePlugins: { + throw: { + actions: { + func: () => { + return { + type: "THROW_FUNC", + payload: "BOOM!" + } + } + }, + reducers: { + "THROW_FUNC": (state, action) => { + throw new Error("this reducer EXPLODES!") + } + } + } + } + } + + }) + + expect(function() { + system.getSystem().throwActions.func() + }).toNotThrow() + }) + + it("should encapsulate thrown errors in a selector", function(){ + + // Given + const system = new System({ + plugins: { + statePlugins: { + throw: { + selectors: { + func: (state, arg1) => { + throw new Error("this selector THROWS!") + } + } + } + } + } + + }) + + expect(system.getSystem().throwSelectors.func).toNotThrow() + }) + + it("should encapsulate thrown errors in a complex selector", function(){ + + // Given + const system = new System({ + plugins: { + statePlugins: { + throw: { + selectors: { + func: (state, arg1) => system => { + throw new Error("this selector THROWS!") + } + } + } + } + } + + }) + + expect(system.getSystem().throwSelectors.func).toNotThrow() + }) + + it("should encapsulate thrown errors in a wrapAction", function(){ + + // Given + const system = new System({ + plugins: { + statePlugins: { + throw: { + actions: { + func: () => { + return { + type: "THROW_FUNC", + payload: "this original action does NOT throw" + } + } + }, + wrapActions: { + func: (ori) => (...args) => { + throw new Error("this wrapAction UNRAVELS EVERYTHING!") + } + } + } + } + } + + }) + + expect(system.getSystem().throwActions.func).toNotThrow() + }) + + it("should encapsulate thrown errors in a wrapSelector", function(){ + + // Given + const system = new System({ + plugins: { + statePlugins: { + throw: { + selectors: { + func: (state, arg1) => { + return 123 + } + }, + wrapSelectors: { + func: (ori) => (...props) => { + return ori(...props) + } + } + } + } + } + + }) + + expect(system.getSystem().throwSelectors.func).toNotThrow() + }) + + describe("components", function() { + it("should catch errors thrown inside of React Component Class render methods", function() { + // Given + // eslint-disable-next-line react/require-render-return + class BrokenComponent extends React.Component { + render() { + throw new Error("This component is broken") + } + } + const system = new System({ + plugins: [ + ViewPlugin, + { + components: { + BrokenComponent + } + } + ] + }) + + // When + var Component = system.getSystem().getComponent("BrokenComponent") + const renderedComponent = render() + + // Then + expect(renderedComponent.text()).toEqual("😱 Could not render BrokenComponent, see the console.") + }) + + it("should catch errors thrown inside of pure component render methods", function() { + // Given + // eslint-disable-next-line react/require-render-return + class BrokenComponent extends PureComponent { + render() { + throw new Error("This component is broken") + } + } + + const system = new System({ + plugins: [ + ViewPlugin, + { + components: { + BrokenComponent + } + } + ] + }) + + // When + var Component = system.getSystem().getComponent("BrokenComponent") + const renderedComponent = render() + + // Then + expect(renderedComponent.text()).toEqual("😱 Could not render BrokenComponent, see the console.") + }) + + it("should catch errors thrown inside of stateless component functions", function() { + // Given + // eslint-disable-next-line react/require-render-return + let BrokenComponent = function BrokenComponent() { throw new Error("This component is broken") } + const system = new System({ + plugins: [ + ViewPlugin, + { + components: { + BrokenComponent + } + } + ] + }) + + // When + var Component = system.getSystem().getComponent("BrokenComponent") + const renderedComponent = render() + + // Then + expect(renderedComponent.text().startsWith("😱 Could not render")).toEqual(true) + }) + + it("should catch errors thrown inside of container components", function() { + // Given + // eslint-disable-next-line react/require-render-return + class BrokenComponent extends React.Component { + render() { + throw new Error("This component is broken") + } + } + + const system = new System({ + plugins: [ + ViewPlugin, + { + components: { + BrokenComponent + } + } + ] + }) + + // When + var Component = system.getSystem().getComponent("BrokenComponent", true) + const renderedComponent = render( + + + + ) + + // Then + expect(renderedComponent.text()).toEqual("😱 Could not render BrokenComponent, see the console.") + }) + }) + }) })