Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

fix($q): treat thrown errors as regular rejections #15213

Conversation

gkalpak
Copy link
Member

@gkalpak gkalpak commented Oct 4, 2016

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
Refactor/Fix (depending on your point of view).

What is the current behavior? (You can also link to an open issue here)
Errors thrown in a promise's onFulfilled or onRejected handlers are treated in a
slightly different manner than regular rejections:
They are passed to the $exceptionHandler() (in addition to being converted to rejections).
See also #3174 and #14745.

What is the new behavior (if this is a feature change)?
The distinction is removed, by skipping the call to $exceptionHandler(), thus treating
thrown errors as regular rejections.

Does this PR introduce a breaking change?
Yes.

Please check if the PR fulfills these requirements

Other information:
Previously, errors thrown in a promise's onFulfilled or onRejected handlers were treated in a
slightly different manner than regular rejections:
They were passed to the $exceptionHandler() (in addition to being converted to rejections).

The reasoning for this behavior was that an uncaught error is different than a regular rejection, as
it can be caused by a programming error, for example. In practice, this turned out to be confusing
or undesirable for users, since neither native promises nor any other popular promise library
distinguishes thrown errors from regular rejections.
(Note: While this behavior does not go against the Promises/A+ spec, it is not prescribed either.)

This commit removes the distinction, by skipping the call to $exceptionHandler(), thus treating
thrown errors as regular rejections.

Note:
Unless explicitly turned off, possibly unhandled rejections will still be caught and passed to the
$exceptionHandler(), so errors thrown due to programming errors and not otherwise handled (with a
subsequent onRejected handler) will not go unnoticed.

BREAKING CHANGE:

Previously, throwing an error from a promise's onFulfilled or onRejection handlers, would result
in passing the error to the $exceptionHandler() (in addition to rejecting the promise with the
error as reason).

Now, a thrown error is treated exactly the same as a regular rejection. This applies to all
services/controllers/filters etc that rely on $q (including built-in services, such as $http and
$route). For example, $http's transformRequest/Response functions or a route's redirectTo
function as well as functions specified in a route's resolve object, will no longer result in a
call to $exceptionHandler() if they throw an error. Other than that, everything will continue to
behave in the same way; i.e. the promises will be rejected, route transition will be cancelled,
$routeChangeError events will be broadcasted etc.

Fixes #3174
Fixes #14745

Previously, errors thrown in a promise's `onFulfilled` or `onRejected` handlers were treated in a
slightly different manner than regular rejections:
They were passed to the `$exceptionHandler()` (in addition to being converted to rejections).

The reasoning for this behavior was that an uncaught error is different than a regular rejection, as
it can be caused by a programming error, for example. In practice, this turned out to be confusing
or undesirable for users, since neither native promises nor any other popular promise library
distinguishes thrown errors from regular rejections.
(Note: While this behavior does not go against the Promises/A+ spec, it is not prescribed either.)

This commit removes the distinction, by skipping the call to `$exceptionHandler()`, thus treating
thrown errors as regular rejections.

**Note:**
Unless explicitly turned off, possibly unhandled rejections will still be caught and passed to the
`$exceptionHandler()`, so errors thrown due to programming errors and not otherwise handled (with a
subsequent `onRejected` handler) will not go unnoticed.

BREAKING CHANGE:

Previously, throwing an error from a promise's `onFulfilled` or `onRejection` handlers, would result
in passing the error to the `$exceptionHandler()` (in addition to rejecting the promise with the
error as reason).

Now, a thrown error is treated exactly the same as a regular rejection. This applies to all
services/controllers/filters etc that rely on `$q` (including built-in services, such as `$http` and
`$route`). For example, `$http`'s `transformRequest/Response` functions or a route's `redirectTo`
function as well as functions specified in a route's `resolve` object, will no longer result in a
call to `$exceptionHandler()` if they throw an error. Other than that, everything will continue to
behave in the same way; i.e. the promises will be rejected, route transition will be cancelled,
`$routeChangeError` events will be broadcasted etc.

Fixes angular#3174
Fixes angular#14745
@gkalpak
Copy link
Member Author

gkalpak commented Oct 4, 2016

Should we document that the $exceptionHandler is called on 1.5.x or earlier (as suggested in #3174 (comment))?
Sounds like low level stuff.

Copy link
Contributor

@petebacondarwin petebacondarwin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few minor things but generally LGTM

@@ -3052,8 +3052,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {

$compileNode.empty();

$templateRequest(templateUrl)
.then(function(content) {
$templateRequest(templateUrl).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you're a "dots at the end of the line" kind of guy eh?
This is not in-keeping with the rest of the codebase

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Knee-jerk reaction, sorry 😞 I'll amend.

(Although, there are some occurrences and I myself have sneaked in a couple of those lately 😛)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a search of the src folder for /\w+\.$/ and didn't find any - although there were lots of matches for sentences in the docs and I might have missed the code ones.

Copy link
Member Author

@gkalpak gkalpak Oct 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a little more lucky with my search (without even taking the test/ directory into account, because of too many occurrences) 😃 :

https://github.com/angular/angular.js/blob/823295f/compare-master-to-stable.js#L75-L81
https://github.com/angular/angular.js/blob/823295f/compare-master-to-stable.js#L93-L104
https://github.com/angular/angular.js/blob/823295f/compare-master-to-stable.js#L110-L124
https://github.com/angular/angular.js/blob/823295f/compare-master-to-stable.js#L132-L142
https://github.com/angular/angular.js/blob/823295f/compare-master-to-stable.js#L154-L155
https://github.com/angular/angular.js/blob/823295f/compare-master-to-stable.js#L160-L164
https://github.com/angular/angular.js/blob/823295f/docs/app/test/errorSpec.js#L6-L7
https://github.com/angular/angular.js/blob/823295f/i18n/e2e/i18n-e2e.js#L73-L82
https://github.com/angular/angular.js/blob/823295f/i18n/e2e/i18n-e2e.js#L171-L172
https://github.com/angular/angular.js/blob/823295f/i18n/src/closureI18nExtractor.js#L93-L100
https://github.com/angular/angular.js/blob/823295f/lib/grunt/utils.js#L206-L207
https://github.com/angular/angular.js/blob/823295f/src/Angular.js#L1406-L1409
https://github.com/angular/angular.js/blob/823295f/src/Angular.js#L1425-L1433
https://github.com/angular/angular.js/blob/823295f/src/AngularPublic.js#L173-L174
https://github.com/angular/angular.js/blob/823295f/src/AngularPublic.js#L218-L223
https://github.com/angular/angular.js/blob/823295f/src/jqLite.js#L153-L154
https://github.com/angular/angular.js/blob/823295f/src/jqLite.js#L412-L413
https://github.com/angular/angular.js/blob/823295f/src/ng/cacheFactory.js#L66-L67
https://github.com/angular/angular.js/blob/823295f/src/ng/compile.js#L3292-L3293
https://github.com/angular/angular.js/blob/823295f/src/ng/exceptionHandler.js#L25-L26
https://github.com/angular/angular.js/blob/823295f/src/ng/http.js#L893-L894
https://github.com/angular/angular.js/blob/823295f/src/ng/interpolate.js#L111-L112
https://github.com/angular/angular.js/blob/823295f/src/ng/sce.js#L42-L44
https://github.com/angular/angular.js/blob/823295f/src/ng/directive/ngClass.js#L300-L301
https://github.com/angular/angular.js/blob/823295f/src/ng/directive/ngClass.js#L359-L362
https://github.com/angular/angular.js/blob/823295f/src/ng/directive/ngClass.js#L407-L410
https://github.com/angular/angular.js/blob/823295f/src/ng/directive/ngCloak.js#L46-L49
https://github.com/angular/angular.js/blob/823295f/src/ng/directive/ngModel.js#L94-L95
https://github.com/angular/angular.js/blob/823295f/src/ng/directive/ngModel.js#L160-L161
https://github.com/angular/angular.js/blob/823295f/src/ng/filter/filters.js#L74-L75
https://github.com/angular/angular.js/blob/823295f/src/ng/filter/filters.js#L563-L570
https://github.com/angular/angular.js/blob/823295f/src/ngAria/aria.js#L56-L57
https://github.com/angular/angular.js/blob/823295f/src/ngCookies/cookies.js#L19-L20
https://github.com/angular/angular.js/blob/823295f/src/ngCookies/cookieStore.js#L3-L4
https://github.com/angular/angular.js/blob/823295f/src/ngResource/resource.js#L431-L432
https://github.com/angular/angular.js/blob/823295f/src/ngResource/resource.js#L477-L478
https://github.com/angular/angular.js/blob/823295f/src/ngResource/resource.js#L489-L490
https://github.com/angular/angular.js/blob/823295f/src/ngRoute/route.js#L28-L30
https://github.com/angular/angular.js/blob/823295f/src/ngRoute/route.js#L680-L685
https://github.com/angular/angular.js/blob/823295f/src/ngRoute/route.js#L727-L728
https://github.com/angular/angular.js/blob/823295f/src/ngRoute/route.js#L753-L760
https://github.com/angular/angular.js/blob/823295f/src/ngSanitize/sanitize.js#L110-L116
https://github.com/angular/angular.js/blob/823295f/src/ngSanitize/sanitize.js#L122-L123
https://github.com/angular/angular.js/blob/823295f/src/ngSanitize/sanitize.js#L131-L132
https://github.com/angular/angular.js/blob/823295f/src/ngSanitize/sanitize.js#L423-L433
https://github.com/angular/angular.js/blob/823295f/src/ngSanitize/filter/linky.js#L91-L92
https://github.com/angular/angular.js/blob/823295f/src/ngSanitize/filter/linky.js#L98-L99
https://github.com/angular/angular.js/blob/823295f/src/ngSanitize/filter/linky.js#L107-L108
https://github.com/angular/angular.js/blob/823295f/src/ngSanitize/filter/linky.js#L115-L117
https://github.com/angular/angular.js/blob/823295f/src/ngSanitize/filter/linky.js#L122-L124
https://github.com/angular/angular.js/blob/823295f/src/ngSanitize/filter/linky.js#L752-L755

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh well...


expect($exceptionHandler.errors[0].message).toMatch(new RegExp(
'^\\[\\$compile:multidir] Multiple directives \\[first, second] asking for ' +
'transclusion on: <p '));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should have a helper function for checking the exception handler message?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be helpful indeed. I'll add one.


expect(function() {
$rootScope.$digest();
$templateRequest('tpl.html').catch(noop);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this noop should be a spy; and we should check that it gets called?

@@ -115,19 +115,17 @@ describe('$templateRequest', function() {
}));

it('should throw an error when the template is not found',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description is no longer accurate.

$exceptionHandler(error);
}
}).
catch(noop);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This catch shouldn't be needed. The one above it is already catching all errors and is not re-throwing or returning a rejected promise, so this should be unreachable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends on what the $exceptionHandler does. Throwing from the $exceptionHandler (as happens in tests) results in a Possibly Unhandled Rejection error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, never dawned on me that would be a valid thing to do.

@gkalpak gkalpak force-pushed the fix-q-not-call-exceptionHandler-for-thrown-errors branch from e3dcd47 to 0c4266d Compare October 5, 2016 11:42
@gkalpak gkalpak closed this in e13eeab Oct 5, 2016
@gkalpak gkalpak deleted the fix-q-not-call-exceptionHandler-for-thrown-errors branch October 5, 2016 11:53
ellimist pushed a commit to ellimist/angular.js that referenced this pull request Mar 15, 2017
Previously, errors thrown in a promise's `onFulfilled` or `onRejected` handlers were treated in a
slightly different manner than regular rejections:
They were passed to the `$exceptionHandler()` (in addition to being converted to rejections).

The reasoning for this behavior was that an uncaught error is different than a regular rejection, as
it can be caused by a programming error, for example. In practice, this turned out to be confusing
or undesirable for users, since neither native promises nor any other popular promise library
distinguishes thrown errors from regular rejections.
(Note: While this behavior does not go against the Promises/A+ spec, it is not prescribed either.)

This commit removes the distinction, by skipping the call to `$exceptionHandler()`, thus treating
thrown errors as regular rejections.

**Note:**
Unless explicitly turned off, possibly unhandled rejections will still be caught and passed to the
`$exceptionHandler()`, so errors thrown due to programming errors and not otherwise handled (with a
subsequent `onRejected` handler) will not go unnoticed.

Fixes angular#3174
Fixes angular#14745

Closes angular#15213

BREAKING CHANGE:

Previously, throwing an error from a promise's `onFulfilled` or `onRejection` handlers, would result
in passing the error to the `$exceptionHandler()` (in addition to rejecting the promise with the
error as reason).

Now, a thrown error is treated exactly the same as a regular rejection. This applies to all
services/controllers/filters etc that rely on `$q` (including built-in services, such as `$http` and
`$route`). For example, `$http`'s `transformRequest/Response` functions or a route's `redirectTo`
function as well as functions specified in a route's `resolve` object, will no longer result in a
call to `$exceptionHandler()` if they throw an error. Other than that, everything will continue to
behave in the same way; i.e. the promises will be rejected, route transition will be cancelled,
`$routeChangeError` events will be broadcasted etc.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
4 participants