Skip to content

Commit 38432f4

Browse files
fix(Rejection): Silence "Error: Uncaught (in Exception)"
Closes #2676
1 parent 6becb12 commit 38432f4

File tree

7 files changed

+52
-24
lines changed

7 files changed

+52
-24
lines changed

src/common/common.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import {isFunction, isString, isArray, isRegExp, isDate} from "./predicates";
88
import { all, any, not, prop, curry } from "./hof";
9+
import {services} from "./coreservices";
910

1011
let w: any = typeof window === 'undefined' ? {} : window;
1112
let angular = w.angular || {};
@@ -538,9 +539,9 @@ function _arraysEq(a1, a2) {
538539
if (a1.length !== a2.length) return false;
539540
return arrayTuples(a1, a2).reduce((b, t) => b && _equals(t[0], t[1]), true);
540541
}
541-
//
542-
//const _addToGroup = (result, keyFn) => (item) =>
543-
// (result[keyFn(item)] = result[keyFn(item)] || []).push(item) && result;
544-
//const groupBy = (array, keyFn) => array.reduce((memo, item) => _addToGroup(memo, keyFn), {});
545-
//
546-
//
542+
543+
// issue #2676
544+
export const silenceUncaughtInPromise = (promise: Promise<any>) =>
545+
promise.catch(e => 0) && promise;
546+
export const silentRejection = (error: any) =>
547+
silenceUncaughtInPromise(services.$q.reject(error));

src/state/stateService.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/** @module state */ /** */
2-
import {extend, defaults } from "../common/common";
2+
import {extend, defaults, silentRejection, silenceUncaughtInPromise} from "../common/common";
33
import {isDefined, isObject, isString} from "../common/predicates";
44
import {Queue} from "../common/queue";
55
import {services} from "../common/coreservices";
@@ -274,7 +274,7 @@ export class StateService {
274274
return this._handleInvalidTargetState(currentPath, ref);
275275

276276
if (!ref.valid())
277-
return services.$q.reject(ref.error());
277+
return <TransitionPromise> silentRejection(ref.error());
278278

279279
/**
280280
* Special handling for Ignored, Aborted, and Redirected transitions
@@ -307,6 +307,8 @@ export class StateService {
307307

308308
let transition = this.router.transitionService.create(currentPath, ref);
309309
let transitionToPromise = transition.run().catch(rejectedTransitionHandler(transition));
310+
silenceUncaughtInPromise(transitionToPromise); // issue #2676
311+
310312
// Return a promise for the transition, which also has the transition object on it.
311313
return extend(transitionToPromise, { transition });
312314
};

src/transition/rejectFactory.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/** @module transition */ /** for typedoc */
22
"use strict";
3-
import {extend} from "../common/common";
4-
import {services} from "../common/coreservices";
3+
import {extend, silentRejection} from "../common/common";
54
import {stringify} from "../common/strings";
65

76
export enum RejectType {
@@ -27,7 +26,7 @@ export class Rejection {
2726
}
2827

2928
toPromise() {
30-
return extend(services.$q.reject(this), { _transitionRejection: this });
29+
return extend(silentRejection(this), { _transitionRejection: this });
3130
}
3231

3332
/** Returns true if the obj is a rejected promise created from the `asPromise` factory */

src/transition/transition.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -487,9 +487,13 @@ export class Transition implements IHookRegistry {
487487

488488
trace.traceTransitionStart(this);
489489

490+
// Chain the next hook off the previous
491+
const appendHookToChain = (prev, nextHook) =>
492+
prev.then(() => nextHook.invokeHook());
493+
490494
// Run the hooks, then resolve or reject the overall deferred in the .then() handler
491495
hookBuilder.asyncHooks()
492-
.reduce((_chain, step) => _chain.then(step.invokeHook.bind(step)), syncResult)
496+
.reduce(appendHookToChain, syncResult)
493497
.then(transitionSuccess, transitionError);
494498

495499
return this.promise;

src/transition/transitionHook.ts

+4-9
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,15 @@ export class TransitionHook {
3434

3535
private isSuperseded = () => this.options.current() !== this.options.transition;
3636

37-
invokeHook(rethrow = false): Promise<any> {
37+
invokeHook(): Promise<any> {
3838
let { options, hookFn, resolveContext } = this;
3939
trace.traceHookInvocation(this, options);
4040
if (options.rejectIfSuperseded && this.isSuperseded()) {
4141
return Rejection.superseded(options.current()).toPromise();
4242
}
4343

44-
try {
45-
var hookResult = hookFn.call(options.bind, this.transition, resolveContext.injector(), this.stateContext);
46-
return this.handleHookResult(hookResult);
47-
} catch (error) {
48-
if (rethrow) throw error;
49-
return services.$q.reject(error);
50-
}
44+
let hookResult = hookFn.call(options.bind, this.transition, resolveContext.injector(), this.stateContext);
45+
return this.handleHookResult(hookResult);
5146
}
5247

5348
/**
@@ -96,7 +91,7 @@ export class TransitionHook {
9691
let results = [];
9792
for (let i = 0; i < hooks.length; i++) {
9893
try {
99-
results.push(hooks[i].invokeHook(true));
94+
results.push(hooks[i].invokeHook());
10095
} catch (exception) {
10196
if (!swallowExceptions) {
10297
return Rejection.aborted(exception).toPromise();

test/ng1/transitionSpec.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@ import {Transition} from "../../src/transition/transition";
1414

1515
describe('transition', function () {
1616

17-
var transitionProvider, matcher, pathFactory, statesMap, queue;
17+
var $exceptionHandler, transitionProvider, matcher, pathFactory, statesMap, queue;
1818

1919
var targetState = function(identifier, params = {}, options?) {
2020
options = options || {};
2121
var stateDefinition = matcher.find(identifier, options.relative);
2222
return new TargetState(identifier, stateDefinition, params, options);
2323
};
2424

25-
beforeEach(module('ui.router', function ($transitionsProvider, $urlMatcherFactoryProvider) {
25+
beforeEach(module('ui.router', function ($transitionsProvider, $urlMatcherFactoryProvider, $exceptionHandlerProvider) {
26+
decorateExceptionHandler($exceptionHandlerProvider);
2627
transitionProvider = $transitionsProvider;
28+
2729
var stateTree = {
2830
first: {},
2931
second: {},
@@ -71,7 +73,8 @@ describe('transition', function () {
7173

7274
var makeTransition;
7375

74-
beforeEach(inject(function ($transitions, $state) {
76+
beforeEach(inject(function ($transitions, $state, _$exceptionHandler_) {
77+
$exceptionHandler = _$exceptionHandler_;
7578
matcher = new StateMatcher(statesMap);
7679
queue.flush($state);
7780
makeTransition = function makeTransition(from, to, options) {
@@ -113,6 +116,7 @@ describe('transition', function () {
113116

114117
it('$transition$.promise should reject on error', inject(function($transitions, $q) {
115118
var result = new PromiseResult();
119+
$exceptionHandler.disabled = true;
116120

117121
transitionProvider.onStart({ from: "*", to: "third" }, function($transition$) {
118122
result.setPromise($transition$.promise);
@@ -126,6 +130,7 @@ describe('transition', function () {
126130

127131
it('$transition$.promise should reject on error in synchronous hooks', inject(function($transitions, $q) {
128132
var result = new PromiseResult();
133+
$exceptionHandler.disabled = true;
129134

130135
transitionProvider.onBefore({ from: "*", to: "third" }, function($transition$) {
131136
result.setPromise($transition$.promise);
@@ -298,6 +303,7 @@ describe('transition', function () {
298303
}));
299304

300305
it('should be called even if other .onSuccess() callbacks fail (throw errors, etc)', inject(function($transitions, $q) {
306+
$exceptionHandler.disabled = true;
301307
transitionProvider.onSuccess({ from: "*", to: "*" }, function() { throw new Error("oops!"); });
302308
transitionProvider.onSuccess({ from: "*", to: "*" }, function(trans) { states.push(trans.to().name); });
303309

@@ -318,6 +324,7 @@ describe('transition', function () {
318324
}));
319325

320326
it('should be called if any part of the transition fails.', inject(function($transitions, $q) {
327+
$exceptionHandler.disabled = true;
321328
transitionProvider.onEnter({ from: "A", entering: "C" }, function() { throw new Error("oops!"); });
322329
transitionProvider.onError({ }, function(trans) { states.push(trans.to().name); });
323330

@@ -327,6 +334,7 @@ describe('transition', function () {
327334
}));
328335

329336
it('should be called for only handlers matching the transition.', inject(function($transitions, $q) {
337+
$exceptionHandler.disabled = true;
330338
transitionProvider.onEnter({ from: "A", entering: "C" }, function() { throw new Error("oops!"); });
331339
transitionProvider.onError({ from: "*", to: "*" }, function() { hooks.push("splatsplat"); });
332340
transitionProvider.onError({ from: "A", to: "C" }, function() { hooks.push("AC"); });

test/testUtilsNg1.js

+19
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,25 @@ function html5Compat(html5mode) {
116116
return (angular.isObject(html5mode) && html5mode.hasOwnProperty("enabled") ? html5mode.enabled : html5mode);
117117
}
118118

119+
/**
120+
* The ng1 $exceptionHandler from angular-mocks will re-throw any exceptions thrown in a Promise.
121+
* This chunk of code decorates the handler, allowing a test to disable that behavior.
122+
* Inject $exceptionHandler and set `$exceptionHandler.disabled = true`
123+
*/
124+
function decorateExceptionHandler($exceptionHandlerProvider) {
125+
var $get = $exceptionHandlerProvider.$get;
126+
127+
$exceptionHandlerProvider.$get = function() {
128+
var realHandler = $get.apply($exceptionHandlerProvider, arguments);
129+
function passThrough(e) {
130+
if (!passThrough['disabled']) {
131+
realHandler.apply(null, arguments);
132+
}
133+
}
134+
return passThrough;
135+
};
136+
}
137+
119138

120139
// Utils for test from core angular
121140
var noop = angular.noop,

0 commit comments

Comments
 (0)