mobx: How should MobX handle errors in computed values and reactions?

As a follow up of #721 and several past issues where people found the logging about errors by MobX confusing. I played a bit with possible error handling strategies, but it is hard to find a best solution, but here are some possibilities:

Some facts up front:

  1. Rethrowing an exception will cause the debugger to pause on the rethrow (especially if “pause on caught exceptions” is not ticked).
  2. Logging an error (at least in Chrome) will always show the stacktrace of the log statement, and not of the exception!

How MobX could handle exceptions:

Solution 1. Never catch exceptions, make sure no state get corrupts by means of try / finally.

Print warning about the uncaught exception.

  1. Pro: No issues with fact 1
  2. Pro: No issues with fact 2.
  3. Con: exception might be silently eaten in userland (leaving only the warning).
  4. Con: Cannot log the exception (what started this issue).
  5. Con: If a reaction throws, other pending reactions / computed values might not run anymore (until a next change), if if they maybe can, as the error could be unrelated. So more stuff then strictly necessary might freeze

This is the current behavior in MobX 2. Making the log message more clear could already improve things. Current:

mobx.js:969 [mobx] An uncaught exception occurred while calculating your computed value, autorun or transformer. Or inside the render() method of an observer based React component. These functions should never throw exceptions as MobX will not always be able to recover from them. Please fix the error reported after this message or enable 'Pause on (caught) exceptions' in your debugger to find the root cause. In: 'Contact@1.displayName'. For more details see https://github.com/mobxjs/mobx/issues/462

Better?

An exception was thrown by a computed value, reaction or observer. Throwing exceptions might lead to unexpected results, please avoid throwing exceptions from derivations. In: 'Contact@1.displayName'.

Solution 2: Consider exceptions a user problem

Don’t try to recover from exceptions at all. After exceptions an app (or at least MobX) is just dead. Simple and efficient. Throwing is forbidden. No issues with fact 1 or 2, exceptions could be accidentally eaten in userland

Solution 3: Don’t let exceptions escape

Catch all exceptions produced by reactions and produced values. Log when they happen

  1. Pro: Simplifies implementation of MobX
  2. Pro: Unrelated reactions are always run
  3. Con: Exceptions might go by easily unnoticed as they are only logged
  4. Con: Logging gives wrong stack mostly (see 2)

Solution 4: Save and rethrow exceptions

Catch all exceptions (like in 3) and rethrow them when either: 1. when the value of a computed value is requested (.get()), 2. or after all pending reactions have run

  1. Pro: Also simplifies implementation of MobX a bit (hardly exception handling needed)
  2. Pro: MobX can always run ‘unrelated’ reactions
  3. Pro: Exceptions are caught and can be logged
  4. Con: Debugger will break at the rethrow (unless ‘break on caught’ is ticked), but stack is correct
  5. Con: Exception might still be eaten in userland. At least log about the rethrow?

Solution 5: Save and rethrow exceptions on separate stack

Same as 4, but without downside 4. Throw the saved exceptions from a new stack using setImmediate, makes it impossible to ‘eat’ the exception, and impossible to actually catch the exception in userland


Note that running everything in sync is usally an advantage as people could catch exceptions in userland, although sometimes it leads by unwary users to problems, e.g.

fetch(data).then(json => {
   try {
      someObservable = JSON.parse(json)
   } catch (e) {
      // ignore JSON exceptions
      // .... but this also eats exceptions thrown by derivations that are run as a result of assigning someObservable!
   }
})

@benjamingr did you give exceptions that have no handler special treatment in bluebird? If I remember when library did go the extra mile by trying to detect this?

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 27 (19 by maintainers)

Commits related to this issue

Most upvoted comments

@mweststrate why is asynchronous rethrow better then just console.error(error)? It seems that async errors can be harder to debug:

console.log('↓ May thow here:');
doTheStuff();
console.log('↑ Any errors?');
// ↓ May thow here:
// ↑ Any errors?
// ...
// Error!

@mweststrate, you have to click (...) and then click on the stack trace again 😃

JavaScript stack traces are determined when the error is created, not when it is last thrown.

an optional spy API that catches the actual error object and attaches it to the error event can be a huge benefit as well. such a solution avoids the logging stacktrace problem while providing valuable informations for tests, debuggers etc.