[BUGFIX lts] Refactor / fix error handling scenarios.#15871
[BUGFIX lts] Refactor / fix error handling scenarios.#15871rwjblue merged 4 commits intoemberjs:masterfrom
Conversation
|
/cc @workmanw for review |
|
|
||
| let dispatchOverride; | ||
| // dispatch error | ||
| export function dispatchError(error) { |
There was a problem hiding this comment.
This was previously only used by RSVP.on('error', ...) (after reverting #14898), and RSVP.on('error', ...) in this PR now does the logic of dispatchError directly (which makes it much clearer that this is RSVP error handling specific).
381307f to
932cd8c
Compare
…se `dispatchError` instead of `onError`. This is so that backburner errors can be caught by the `Test.adapter`. Fixes emberjs#14864." This reverts commit 196442d which essentially made all error handling for things that are run-wrapped async, dramatically impacting development ergonomics. The originally reported issue is a _very real_ problem that we need to guard against. To reproduce that issue, the following conditions must exist: * The application must implement `Ember.onerror` in a way that does not rethrow errors. * Throw an error during anything that uses `run.join`. The example scenario had a template like this: ```hbs <button {{action 'throwsAnError'}}>Click me!</button> ``` To fix this error swallowing behavior, the commit being reverted made all errors hit within the run loop use `dispatchError`, which (during tests) has the default implementation of invoking `QUnit`'s `assert.ok(false)`. Unfortunately, this meant that it is now impossible to use a standard `try` / `catch` to catch errors thrown within anything "run-wrapped". For example, these patterns were no longer possible after the commit in question: ```js try { Ember.run(() => { throw new Error('This error should be catchable'); }); } catch(e) { // this will never be hit during tests... } ``` This ultimately breaks a large number of test suites that rely (rightfully so!) on being able to do things like: ```js module('service:foo-bar', function(hooks) { setupTest(hooks); hooks.beforeEach(() => { this.owner.register('service:whatever', Ember.Service.extend({ someMethod(argumentHere) { Ember.assert('Some random argument validation here', !argumentHere); } }); }); test('throws when argumentHere is missing', function(assert) { let subject = this.owner.lookup('service:foo-bar'); assert.throws(() => { run(() => subject.someMethod()); }, /random argument validation/); }); }); ``` The ergonomics of breaking standard JS `try` / `catch` is too much, and therefore the original commit is being reverted.
Known permutations:
* Testing
* With `Ember.onerror`
* Sync Backburner (`run` and `run.join`)
* Async Backburner (`run.later`, `run.next`)
* RSVP
* Without `Ember.onerror`
* Sync Backburner (`run` and `run.join`)
* Async Backburner (`run.later`, `run.next`)
* RSVP
* Development / Production
* With `Ember.onerror`
* Sync Backburner (`run` and `run.join`)
* Async Backburner (`run.later`, `run.next`)
* RSVP
* Without `Ember.onerror`
* Sync Backburner (`run` and `run.join`)
* Async Backburner (`run.later`, `run.next`)
* RSVP
This commit adds tests for all scenarios.
…ing. In the majority of testing frameworks (e.g. Mocha, QUnit, Jasmine) a global error handler is attached to `window.onerror`. When an uncaught exception is thrown, this global handler is invoked and the test suite will properly fail. Requiring that the provided test adapter provide an exception method is strictly worse in nearly all cases than allowing the error to be bubbled upwards and caught/handled by either `Ember.onerror` (if present) or `window.onerror` (generally from the test framework). This commit makes `Ember.Test.adapter.exception` optional. When not present, errors during testing will be handled by existing "normal" means.
When used in Ember, `RSVP` is configured to settle within
Backburner's configured `actions` queue.
Prior to this change, any unhandled promise rejections would:
* `Ember.testing === true`
* When `Ember.Test.adapter` was registered the `Test.adapter`'s
`exception` method would be called, and the rejection would be logged
to the console.
* When `Ember.Test.adapter` was not registered, the `defaultDispatch`
implementation would re-throw the error, and since RSVP settles in the
run loop this means that the re-thrown error would be caught by the
currently flushing Backburner queue's `invokeWithOnError` which sends
any errors to `Ember.onerror` (if present). If `Ember.onerror` was not
present, the exception would bubble up the "normal" unhandled
exception system (and ultimately to `window.onerror`).
* `Ember.testing === false`
* When `Ember.onerror` is present, it would be invoked with the
rejection reason.
* When `Ember.onerror` is not present, the rejection reason would be
logged to the console.
After this change:
* When `Ember.Test.adapter` is present, its `exception` method is
invoked with the rejection.
* Otherwise the rejection reason is rethrown.
The benefits of this are:
* It is now possible to debug rejected promises via "normal" JS
exception debugging (e.g. break on uncaught exception).
* There are many fewer decision points, making it much easier to grok
what is going on.
932cd8c to
0f3f49c
Compare
|
Rebased after landing #15874 to kick off another CI run using the latest Backburner version... |
|
Folks stumbling across this PR may be interested: @hjdivad and I have just published an updated version of ember-test-friendly-error-handler addon which makes creating an |
|
From the original PR description:
Finally have this implemented: emberjs/ember-qunit#304. |
This PR has a few very small changes to the error handling system, but has a large impact to developer ergonomics.
The primary changes in this PR are:
Revert changes made in #14898
#14898 essentially made all error handling for things that are run-wrapped uncatchable, dramatically impacting development ergonomics.
The originally reported issue is a very real problem that we need to guard against. To reproduce that issue, the following conditions must exist:
Ember.onerrorin a way that does not rethrow errors.run.join. The example scenario had a template like this:To fix this error swallowing behavior, the commit being reverted made all errors hit within the run loop use
dispatchError, which (during tests) has the default implementation of invokingQUnit'sassert.ok(false). Unfortunately, this meant that it is now impossible to use a standardtry/catchto catch errors thrown within anything "run-wrapped".For example, these patterns were no longer possible after the commit in question:
This ultimately breaks a large number of test suites that rely (rightfully so!) on being able to do things like:
The ergonomics of breaking standard JS
try/catchis too much of a step backwards, so it is being reverted.As mentioned earlier, the underlying problem reported in #14864 is very real and must be addressed also. Therefore, in order to prevent
Ember.onerrorfrom swallowing errors during testsember-qunit(andember-mocha) will implement checks to ensure thatEmber.onerror(if present) properly rethrows errors during testing. The following is psuedo code of what that might look like:Remove the requirement of
Ember.Test.Adapter's to implement theexceptionmethod.In the majority of testing frameworks (e.g. Mocha, QUnit, Jasmine) a global error handler is attached to
window.onerror. When an uncaught exception is thrown, this global handler is invoked and the test suite will properly fail.Requiring that the provided test adapter provide an exception method is strictly worse (in all cases) than allowing the error to be bubbled upwards and caught/handled by either
Ember.onerror(if present) orwindow.onerror(generally from the test framework).Over time
Ember.Test.adapter.exceptionshould be deprecated in favor of relying onEmber.onerrorand/orwindow.onerror.Simplify unhandled promise rejection bubbling.
When used in Ember,
RSVPis configured to settle within Backburner's configuredactionsqueue.Prior to this change, any unhandled promise rejections would:
Ember.testing === trueEmber.Test.adapterwas registered theTest.adapter'sexceptionmethod would be called, and the rejection would be logged to the console.Ember.Test.adapterwas not registered, thedefaultDispatchimplementation would re-throw the error, and since RSVP settles in the run loop this means that the re-thrown error would be caught by the currently flushing Backburner queue'sinvokeWithOnErrorwhich sends any errors toEmber.onerror(if present). IfEmber.onerrorwas not present, the exception would bubble up the "normal" unhandled exception system (and ultimately towindow.onerror).Ember.testing === falseEmber.onerroris present, it would be invoked with the rejection reason.Ember.onerroris not present, the rejection reason would be logged to the console.After this change:
Ember.Test.adapteris present, itsexceptionmethod is invoked with the rejection.The benefits of this are:
The majority of the changes are in new tests being added, I attempted to ensure that each of the following were under test:
Ember.Test.adapterEmber.onerrorrunandrun.join)run.later,run.next)Ember.onerrorrunandrun.join)run.later,run.next)Ember.Test.adapterEmber.onerrorrunandrun.join)run.later,run.next)Ember.onerrorrunandrun.join)run.later,run.next)Ember.onerrorrunandrun.join)run.later,run.next)Ember.onerrorrunandrun.join)run.later,run.next)Each commit in this PR attempts to "stand on its own" and explain the reasoning for the changes they contain. As such, it may be easier to review commit by commit instead of all at once.