Skip to content

Instantly share code, notes, and snippets.

@TiddoLangerak
Created July 20, 2016 08:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TiddoLangerak/059b98ffe9aad173befb37cb6a9571af to your computer and use it in GitHub Desktop.
Save TiddoLangerak/059b98ffe9aad173befb37cb6a9571af to your computer and use it in GitHub Desktop.
Angular module that monkey patches $q to detect and log unhandled promise rejection. Only tested with 1.4 and highly likely to break in newer releases. Note that this functionality is build in starting from Angular 1.6.
import angular from 'angular';
/**
* Angular doesn't yet have build in support to detect rejected promises without rejection handlers.
* This module monkey patches $q in order to implement such handling. The code below will contain
* many anti-patterns, unfortunately this is the only way to get this detection to work.
*
* Usage: depend on this module, the rest will be done for you
*
* Note that this is specifically written for angular 1.4.x and it is likely to break in later versions.
* Please test this properly when upgrading Angular.
*/
export default angular.module('mm.unhandledPromiseRejectionDetection', [])
.run(($q, $rootScope) => {
'ngInject';
//We need to monkey patch the Defer prototype. Since we don't have direct access to it we
//just instantiate a deferred object, and extract the prototype from it
const deferProto = Object.getPrototypeOf($q.defer());
//We can then wrap the internal defer.$$reject function to add detection for unhandled rejections.
//We do still need a reference to the original reject function, such that we can delegate to it
const originalReject = deferProto.$$reject;
//Due to promise chaining we can encounter the same rejection more than once.
//we cache them in a weak set to prevent these from being logged multiple times
//This isn't a foolproof solution though, since it only works with objects. Non-objects thrown
//as errors will be logged multiple times
const unhandledCache = new WeakSet();
//NOTE: no lambda, we need `this`
deferProto.$$reject = function(rejection) {
//When timeouts/intervals are cancelled the promise is rejected with the canceled message.
//We don't want to report these, so we filter them out
if (rejection === 'canceled') {
return originalReject.call(this, rejection);
}
if (typeof rejection === 'object') {
if (unhandledCache.has(rejection)) {
//chained call, let's do nothing
return originalReject.call(this, rejection);
} else {
unhandledCache.add(rejection);
}
}
//The timing and order of this code is of crucial important:
//- If the unhandled rejection detection is executed too early then callbacks
// may not have been registered for promises that directly reject, resulting in false positives
// e.g. $q.reject('foo').then(noop, errorHandler)
//- If the callback counter code is executed too late then the pending queue has already been
// cleared, resulting in a 100% false positive rate
//
//The original $q.$$reject function (for ng 1.4) uses $rootScope.$evalAsync to delay the execution
//of the rejection handlers. If we therefore also use $evalAsync *before* delegation then our
//callback will be executed just before the rejection handlers are called, and thus exactly in
//time to detect unhandled promise rejections.
$rootScope.$evalAsync(() => {
//Internally promises keep a list of pending continuations. This is a list of tuples, each tuple
//containing a result deferred (for chaining), fulfill callback, reject callback and progress callback.
//We test here if there is any pending continuation that has a rejection callback
//
//To make things even more fun: sometimes there isn't a direct rejection callback and the error
//falls through the chain before hitting a callback, which we also consider to be valid
//error handling. We therefore also need to recursively check the pending callbacks for the
//result deferred
const pending = this.promise.$$state.pending || [];
const hasRejectionCallback = handlesRejection(pending);
if (!hasRejectionCallback) {
console.error("Unhandled promise rejection:", rejection);
}
});
return originalReject.call(this, rejection);
}
/**
* Checks if a list of promise continuations handles rejections
*/
function handlesRejection(pending) {
return pending.some(([result, fulfillCb, rejectCb]) => {
const chained = result.promise.$$state.pending || [];
return Boolean(rejectCb) || handlesRejection(chained);
});
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment