-
Notifications
You must be signed in to change notification settings - Fork 27.4k
feat($q): report promises with non rejection callback #13662
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -216,21 +216,58 @@ | |
* | ||
* @returns {Promise} The newly created promise. | ||
*/ | ||
/** | ||
* @ngdoc provider | ||
* @name $qProvider | ||
* | ||
* @description | ||
*/ | ||
function $QProvider() { | ||
|
||
var errorOnUnhandledRejections = true; | ||
this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { | ||
return qFactory(function(callback) { | ||
$rootScope.$evalAsync(callback); | ||
}, $exceptionHandler); | ||
}, $exceptionHandler, errorOnUnhandledRejections); | ||
}]; | ||
|
||
/** | ||
* @ngdoc method | ||
* @name $qProvider#errorOnUnhandledRejections | ||
* @kind function | ||
* | ||
* @description | ||
* Retrieves or overrides whether to generate an error when a rejected promise is not handled. | ||
* | ||
* @param {boolean=} value Whether to generate an error when a rejected promise is not handled. | ||
* @returns {boolean|ng.$qProvider} Current value when called without a new value or self for | ||
* chaining otherwise. | ||
*/ | ||
this.errorOnUnhandledRejections = function(value) { | ||
if (isDefined(value)) { | ||
errorOnUnhandledRejections = value; | ||
return this; | ||
} else { | ||
return errorOnUnhandledRejections; | ||
} | ||
}; | ||
} | ||
|
||
function $$QProvider() { | ||
var errorOnUnhandledRejections = true; | ||
this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { | ||
return qFactory(function(callback) { | ||
$browser.defer(callback); | ||
}, $exceptionHandler); | ||
}, $exceptionHandler, errorOnUnhandledRejections); | ||
}]; | ||
|
||
this.errorOnUnhandledRejections = function(value) { | ||
if (isDefined(value)) { | ||
errorOnUnhandledRejections = value; | ||
return this; | ||
} else { | ||
return errorOnUnhandledRejections; | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
|
@@ -239,10 +276,14 @@ function $$QProvider() { | |
* @param {function(function)} nextTick Function for executing functions in the next turn. | ||
* @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for | ||
* debugging purposes. | ||
@ param {=boolean} errorOnUnhandledRejections Whether an error should be generated on unhandled | ||
* promises rejections. | ||
* @returns {object} Promise manager. | ||
*/ | ||
function qFactory(nextTick, exceptionHandler) { | ||
function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { | ||
var $qMinErr = minErr('$q', TypeError); | ||
var queueSize = 0; | ||
var checkQueue = []; | ||
|
||
/** | ||
* @ngdoc method | ||
|
@@ -307,28 +348,62 @@ function qFactory(nextTick, exceptionHandler) { | |
pending = state.pending; | ||
state.processScheduled = false; | ||
state.pending = undefined; | ||
for (var i = 0, ii = pending.length; i < ii; ++i) { | ||
deferred = pending[i][0]; | ||
fn = pending[i][state.status]; | ||
try { | ||
if (isFunction(fn)) { | ||
deferred.resolve(fn(state.value)); | ||
} else if (state.status === 1) { | ||
deferred.resolve(state.value); | ||
} else { | ||
deferred.reject(state.value); | ||
try { | ||
for (var i = 0, ii = pending.length; i < ii; ++i) { | ||
state.pur = true; | ||
deferred = pending[i][0]; | ||
fn = pending[i][state.status]; | ||
try { | ||
if (isFunction(fn)) { | ||
deferred.resolve(fn(state.value)); | ||
} else if (state.status === 1) { | ||
deferred.resolve(state.value); | ||
} else { | ||
deferred.reject(state.value); | ||
} | ||
} catch (e) { | ||
deferred.reject(e); | ||
exceptionHandler(e); | ||
} | ||
} catch (e) { | ||
deferred.reject(e); | ||
exceptionHandler(e); | ||
} | ||
} finally { | ||
--queueSize; | ||
if (errorOnUnhandledRejections && queueSize === 0) { | ||
nextTick(processChecksFn()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here too it might be better to not call this is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for |
||
} | ||
} | ||
} | ||
|
||
function processChecks() { | ||
while (!queueSize && checkQueue.length) { | ||
var toCheck = checkQueue.shift(); | ||
if (!toCheck.pur) { | ||
toCheck.pur = true; | ||
var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value); | ||
exceptionHandler(errorMessage); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Out of curiosity, why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @amalitsky This code path is only used if the value thrown in a promise callback was not an error. We could construct an error object here but it wouldn't be of much value as its stack would end up here and not in the place from where the error comes. To be useful, only errors need to be thrown in promise callbacks and not other values. Which is a good idea in general. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mgol, wouldn't path also have a reference to the promise invocation? |
||
} | ||
} | ||
} | ||
|
||
function processChecksFn() { | ||
return function() { processChecks(); }; | ||
} | ||
|
||
function processQueueFn(state) { | ||
return function() { processQueue(state); }; | ||
} | ||
|
||
function scheduleProcessQueue(state) { | ||
if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !state.pur) { | ||
if (queueSize === 0 && checkQueue.length === 0) { | ||
nextTick(processChecksFn()); | ||
} | ||
checkQueue.push(state); | ||
} | ||
if (state.processScheduled || !state.pending) return; | ||
state.processScheduled = true; | ||
nextTick(function() { processQueue(state); }); | ||
++queueSize; | ||
nextTick(processQueueFn(state)); | ||
} | ||
|
||
function Deferred() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -701,12 +701,9 @@ angular.module('ngResource', ['ng']). | |
response.resource = value; | ||
|
||
return response; | ||
}, function(response) { | ||
(error || noop)(response); | ||
return $q.reject(response); | ||
}); | ||
|
||
promise['finally'](function() { | ||
promise = promise['finally'](function() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OOC, does this make any difference (in this particular case, where the code inside the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code inside needs to run once the http request is resolved or canceled. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I meant (assuming that the finally callback does not return a promise and does not throw any errors), does it make any difference to have: promise.finally(...)
promise.then(...) vs promise = promise.finally(...)
promise.then(...) But thinking about it again, the former has the issue that is creates a new promise that will remain unhandled if |
||
value.$resolved = true; | ||
if (!isInstanceCall && cancellable) { | ||
value.$cancelRequest = angular.noop; | ||
|
@@ -721,21 +718,32 @@ angular.module('ngResource', ['ng']). | |
(success || noop)(value, response.headers); | ||
return value; | ||
}, | ||
responseErrorInterceptor); | ||
responseErrorInterceptor || error ? | ||
function(response) { | ||
(error || noop)(response); | ||
(responseErrorInterceptor || noop)(response); | ||
return response; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this changing the current behavior ? Or maybe something like: promise = promise.then(
function(response) {
var value = responseInterceptor(response);
(success || noop)(value, response.headers);
return value;
}, function(response) {
(error || noop)(response);
return $q.reject(response);
}).catch(responseErrorInterceptor); (Although this is also subtly different than the current behavior if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The proposed alternative has the issue that if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The alternative would be to change |
||
} | ||
: undefined); | ||
|
||
if (!isInstanceCall) { | ||
// we are creating instance / collection | ||
// - set the initial promise | ||
// - return the instance / collection | ||
value.$promise = promise; | ||
value.$resolved = false; | ||
if (cancellable) value.$cancelRequest = timeoutDeferred.resolve; | ||
if (cancellable) value.$cancelRequest = cancelRequest; | ||
|
||
return value; | ||
} | ||
|
||
// instance call | ||
return promise; | ||
|
||
function cancelRequest(value) { | ||
promise.catch(noop); | ||
timeoutDeferred.resolve(value); | ||
} | ||
}; | ||
|
||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder where this method shows up in the docs (considering there are no docs for
$qProvider
).I think there should "standalone" docs for
$qProvider
(even if just containing this method), but it can be tackled in a follow-up PR.