diff --git a/src/core/reaction.ts b/src/core/reaction.ts index de2af0ff1..7a8de89ad 100644 --- a/src/core/reaction.ts +++ b/src/core/reaction.ts @@ -184,8 +184,21 @@ function runReactionsHelper() { + ` Probably there is a cycle in the reactive function: ${allReactions[0]}`); } let remainingReactions = allReactions.splice(0); - for (let i = 0, l = remainingReactions.length; i < l; i++) - remainingReactions[i].runReaction(); + for (let i = 0, l = remainingReactions.length; i < l; i++) { + let hasError = true; + try { + remainingReactions[i].runReaction(); + hasError = false; + } finally { + if (hasError) { + // Save all not-yet-run reactions + globalState.pendingReactions.push(...remainingReactions.slice(i + 1)); + // This fancy recursive construction makes sure all other reactions that still can be run are run, + // before throwing the current exception (without losing the stack) + runReactionsHelper(); + } + } + } } globalState.isRunningReactions = false; } diff --git a/test/errorhandling.js b/test/errorhandling.js index bac1e8ce3..568817442 100644 --- a/test/errorhandling.js +++ b/test/errorhandling.js @@ -128,6 +128,34 @@ test('exception in autorun can be recovered from', t => { t.end() }) +test('multiple autoruns with exceptions are handled correctly', t => { + var a = mobx.observable(1) + var values = [] + var d1 = mobx.autorun(() => values.push("a" + a.get())) + var d2 = mobx.autorun(() => { + if (a.get() === 2) + throw /Uhoh/ + values.push("b" + a.get()) + }) + var d3 = mobx.autorun(() => values.push("c" + a.get())) + + t.deepEqual(values, ["a1", "b1", "c1"]) + values.splice(0) + + t.throws(() => a.set(2), /Uhoh/) + checkGlobalState(t) + + t.deepEqual(values.sort(), ["a2", "c2"]) // order is irrelevant + values.splice(0) + + a.set(3) + t.deepEqual(values.sort(), ["a3", "b3", "c3"]) // order is irrelevant + + checkGlobalState(t) + d1(); d2(); d3() + t.end() +}) + test('deny state changes in views', function(t) { var x = observable(3); var z = observable(5);