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.")
+ })
+ })
+ })
})