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

Commit c9dffde

Browse files
committed
feat($q): report promises with non rejection callback
Rejected promises that do not have a callback to handle the rejection report this to $exceptionHandler so they can be logged to the console. BREAKING CHANGE Unhandled rejected promises will be logged to $exceptionHandler. Tests that depend on specific order or number of messages in $exceptionHandler will need to handle rejected promises report. Closes: #13653 Closes: #7992
1 parent 0ece2d5 commit c9dffde

File tree

13 files changed

+192
-45
lines changed

13 files changed

+192
-45
lines changed

docs/app/e2e/api-docs/service-pages.scenario.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe("service pages", function() {
1010

1111
browser.get('build/docs/index.html#!/api/ng/service/$q');
1212
providerLink = element.all(by.css('ol.api-profile-header-structure li a')).first();
13-
expect(providerLink.getText()).not.toEqual('- $qProvider');
13+
expect(providerLink.getText()).not.toEqual('- $compileProvider');
1414
expect(providerLink.getAttribute('href')).not.toMatch(/api\/ng\/provider\/\$compileProvider/);
1515
});
1616

@@ -19,4 +19,4 @@ describe("service pages", function() {
1919
expect(element.all(by.css('.input-arguments p em')).first().getText()).toContain('(default: 0)');
2020
});
2121

22-
});
22+
});

src/ng/compile.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2818,7 +2818,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
28182818
childBoundTranscludeFn);
28192819
}
28202820
linkQueue = null;
2821-
});
2821+
}).catch(noop);
28222822

28232823
return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) {
28242824
var childBoundTranscludeFn = boundTranscludeFn;

src/ng/interval.js

+2
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ function $IntervalProvider() {
188188
*/
189189
interval.cancel = function(promise) {
190190
if (promise && promise.$$intervalId in intervals) {
191+
// Interval cancels should not report as unhandled promise.
192+
intervals[promise.$$intervalId].promise.catch(noop);
191193
intervals[promise.$$intervalId].reject('canceled');
192194
$window.clearInterval(promise.$$intervalId);
193195
delete intervals[promise.$$intervalId];

src/ng/q.js

+93-18
Original file line numberDiff line numberDiff line change
@@ -216,21 +216,58 @@
216216
*
217217
* @returns {Promise} The newly created promise.
218218
*/
219+
/**
220+
* @ngdoc provider
221+
* @name $qProvider
222+
*
223+
* @description
224+
*/
219225
function $QProvider() {
220-
226+
var errorOnUnhandledRejections = true;
221227
this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) {
222228
return qFactory(function(callback) {
223229
$rootScope.$evalAsync(callback);
224-
}, $exceptionHandler);
230+
}, $exceptionHandler, errorOnUnhandledRejections);
225231
}];
232+
233+
/**
234+
* @ngdoc method
235+
* @name $qProvider#errorOnUnhandledRejections
236+
* @kind function
237+
*
238+
* @description
239+
* Retrieves or overrides whether to generate an error when a rejected promise is not handled.
240+
*
241+
* @param {boolean=} value Whether to generate an error when a rejected promise is not handled.
242+
* @returns {boolean|ng.$qProvider} Current value when called without a new value or self for
243+
* chaining otherwise.
244+
*/
245+
this.errorOnUnhandledRejections = function(value) {
246+
if (isDefined(value)) {
247+
errorOnUnhandledRejections = value;
248+
return this;
249+
} else {
250+
return errorOnUnhandledRejections;
251+
}
252+
};
226253
}
227254

228255
function $$QProvider() {
256+
var errorOnUnhandledRejections = true;
229257
this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) {
230258
return qFactory(function(callback) {
231259
$browser.defer(callback);
232-
}, $exceptionHandler);
260+
}, $exceptionHandler, errorOnUnhandledRejections);
233261
}];
262+
263+
this.errorOnUnhandledRejections = function(value) {
264+
if (isDefined(value)) {
265+
errorOnUnhandledRejections = value;
266+
return this;
267+
} else {
268+
return errorOnUnhandledRejections;
269+
}
270+
};
234271
}
235272

236273
/**
@@ -239,10 +276,14 @@ function $$QProvider() {
239276
* @param {function(function)} nextTick Function for executing functions in the next turn.
240277
* @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for
241278
* debugging purposes.
279+
@ param {=boolean} errorOnUnhandledRejections Whether an error should be generated on unhandled
280+
* promises rejections.
242281
* @returns {object} Promise manager.
243282
*/
244-
function qFactory(nextTick, exceptionHandler) {
283+
function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
245284
var $qMinErr = minErr('$q', TypeError);
285+
var queueSize = 0;
286+
var checkQueue = [];
246287

247288
/**
248289
* @ngdoc method
@@ -307,28 +348,62 @@ function qFactory(nextTick, exceptionHandler) {
307348
pending = state.pending;
308349
state.processScheduled = false;
309350
state.pending = undefined;
310-
for (var i = 0, ii = pending.length; i < ii; ++i) {
311-
deferred = pending[i][0];
312-
fn = pending[i][state.status];
313-
try {
314-
if (isFunction(fn)) {
315-
deferred.resolve(fn(state.value));
316-
} else if (state.status === 1) {
317-
deferred.resolve(state.value);
318-
} else {
319-
deferred.reject(state.value);
351+
try {
352+
for (var i = 0, ii = pending.length; i < ii; ++i) {
353+
state.pur = true;
354+
deferred = pending[i][0];
355+
fn = pending[i][state.status];
356+
try {
357+
if (isFunction(fn)) {
358+
deferred.resolve(fn(state.value));
359+
} else if (state.status === 1) {
360+
deferred.resolve(state.value);
361+
} else {
362+
deferred.reject(state.value);
363+
}
364+
} catch (e) {
365+
deferred.reject(e);
366+
exceptionHandler(e);
320367
}
321-
} catch (e) {
322-
deferred.reject(e);
323-
exceptionHandler(e);
368+
}
369+
} finally {
370+
--queueSize;
371+
if (errorOnUnhandledRejections && queueSize === 0) {
372+
nextTick(processChecksFn());
373+
}
374+
}
375+
}
376+
377+
function processChecks() {
378+
while (!queueSize && checkQueue.length) {
379+
var toCheck = checkQueue.shift();
380+
if (!toCheck.pur) {
381+
toCheck.pur = true;
382+
var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value);
383+
exceptionHandler(errorMessage);
324384
}
325385
}
326386
}
327387

388+
function processChecksFn() {
389+
return function() { processChecks(); };
390+
}
391+
392+
function processQueueFn(state) {
393+
return function() { processQueue(state); };
394+
}
395+
328396
function scheduleProcessQueue(state) {
397+
if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !state.pur) {
398+
if (queueSize === 0 && checkQueue.length === 0) {
399+
nextTick(processChecksFn());
400+
}
401+
checkQueue.push(state);
402+
}
329403
if (state.processScheduled || !state.pending) return;
330404
state.processScheduled = true;
331-
nextTick(function() { processQueue(state); });
405+
++queueSize;
406+
nextTick(processQueueFn(state));
332407
}
333408

334409
function Deferred() {

src/ng/timeout.js

+2
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ function $TimeoutProvider() {
8585
*/
8686
timeout.cancel = function(promise) {
8787
if (promise && promise.$$timeoutId in deferreds) {
88+
// Timeout cancels should not report an unhandled promise.
89+
deferreds[promise.$$timeoutId].promise.catch(noop);
8890
deferreds[promise.$$timeoutId].reject('canceled');
8991
delete deferreds[promise.$$timeoutId];
9092
return $browser.defer.cancel(promise.$$timeoutId);

src/ngMock/angular-mocks.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ angular.mock.$IntervalProvider = function() {
445445
promise = deferred.promise;
446446

447447
count = (angular.isDefined(count)) ? count : 0;
448-
promise.then(null, null, (!hasParams) ? fn : function() {
448+
promise.then(null, function() {}, (!hasParams) ? fn : function() {
449449
fn.apply(null, args);
450450
});
451451

@@ -505,6 +505,7 @@ angular.mock.$IntervalProvider = function() {
505505
});
506506

507507
if (angular.isDefined(fnIndex)) {
508+
repeatFns[fnIndex].deferred.promise.then(undefined, function() {});
508509
repeatFns[fnIndex].deferred.reject('canceled');
509510
repeatFns.splice(fnIndex, 1);
510511
return true;

src/ngResource/resource.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -706,13 +706,14 @@ angular.module('ngResource', ['ng']).
706706
return $q.reject(response);
707707
});
708708

709-
promise['finally'](function() {
709+
promise = promise['finally'](function(response) {
710710
value.$resolved = true;
711711
if (!isInstanceCall && cancellable) {
712712
value.$cancelRequest = angular.noop;
713713
$timeout.cancel(numericTimeoutPromise);
714714
timeoutDeferred = numericTimeoutPromise = httpConfig.timeout = null;
715715
}
716+
return response;
716717
});
717718

718719
promise = promise.then(

test/ng/animateRunnerSpec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ describe("$$AnimateRunner", function() {
206206
var animationComplete = false;
207207
runner.finally(function() {
208208
animationComplete = true;
209-
});
209+
}).then(noop, noop);
210210
runner[method]();
211211
$rootScope.$digest();
212212
expect(animationComplete).toBe(true);

test/ng/httpSpec.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1031,7 +1031,7 @@ describe('$http', function() {
10311031

10321032
it('should $apply after error callback', function() {
10331033
$httpBackend.when('GET').respond(404);
1034-
$http({method: 'GET', url: '/some'});
1034+
$http({method: 'GET', url: '/some'}).catch(noop);
10351035
$httpBackend.flush();
10361036
expect($rootScope.$apply).toHaveBeenCalledOnce();
10371037
});
@@ -1432,7 +1432,7 @@ describe('$http', function() {
14321432

14331433
function doFirstCacheRequest(method, respStatus, headers) {
14341434
$httpBackend.expect(method || 'GET', '/url').respond(respStatus || 200, 'content', headers);
1435-
$http({method: method || 'GET', url: '/url', cache: cache});
1435+
$http({method: method || 'GET', url: '/url', cache: cache}).catch(noop);
14361436
$httpBackend.flush();
14371437
}
14381438

@@ -1640,7 +1640,7 @@ describe('$http', function() {
16401640

16411641
it('should preserve config object when rejecting from pending cache', function() {
16421642
$httpBackend.expect('GET', '/url').respond(404, 'content');
1643-
$http({method: 'GET', url: '/url', cache: cache, headers: {foo: 'bar'}});
1643+
$http({method: 'GET', url: '/url', cache: cache, headers: {foo: 'bar'}}).catch(noop);
16441644

16451645
$http({method: 'GET', url: '/url', cache: cache, headers: {foo: 'baz'}}).catch(callback);
16461646
$httpBackend.flush();

0 commit comments

Comments
 (0)