-
Notifications
You must be signed in to change notification settings - Fork 27.4k
Add traceback to unhandled promise rejections, Fixes: #14631 #15527
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 | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -381,7 +381,11 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { | ||||||||||
if (!toCheck.pur) { | |||||||||||
toCheck.pur = true; | |||||||||||
var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value); | |||||||||||
exceptionHandler(errorMessage); | |||||||||||
if (toCheck.value instanceof Error) { | |||||||||||
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. Though I don't think it necessarily a concern here, do we care that this will not identify errors that originate from an iframe? Don't know how common of a use case that 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. I doubt it's common but if it turns out to be an issue we can revisit. Ideally we would convert to a string (via
to:
which is less pleasant. We can discuss it but let's do it in a separate issue if desired. |
|||||||||||
exceptionHandler(toCheck.value, 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. Is this the correct order? It looks weird when the error message is after the stack trace. 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. From https://docs.angularjs.org/api/ng/service/$exceptionHandler $exceptionHandler(exception, [cause]);
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. 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. There is nothing in the function $ExceptionHandlerProvider() {
this.$get = ['$log', function($log) {
return function(exception, cause) {
$log.error.apply($log, arguments);
};
}];
} I wonder what's the reason for the current documentation. @gkalpak, @petebacondarwin does any one of you know? 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. what about just doing exceptionHandler(toCheck.value, 'Possibly unhandled rejection') with no ifs or errorMessage generation. 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. That sounds sensible but I'd still swap the order of operations. Note that currently we are trying to show the exceptionHandler('Possibly unhandled rejection', toCheck.value); but that would require updating the docs to no longer require the cause to be the second parameter (no code changes required as it already works as I mentioned before). I'd just want to know if there's any reason for the current signature so let's see what others say. :) 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 my custom $exceptionHandlers are configured to pass the first argument as the Error value, as per the documentation. I presume others follow the same documentation also. 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. @graingert Is the issue that some automated tools might only pick up the first argument and discard others, losing the error context? Because otherwise, in Angular source, the order influences only one thing - the order in which messages/errors are logged. And it makes more sense to log the summary first and the error second. In other words, the following: exceptionHandler('Possibly unhandled rejection', toCheck.value); is analogous to: exceptionHandler('Possibly unhandled rejection: ' + toDebugString(toCheck.value)); just with more details printed to the console as the error stack is not lost. 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 swapping the types of the parameters of $exceptionHandler is another issue for another PR, and would be a breaking change. 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. OK, fair enough. In that case, maybe it's better to leave it as it is for now; your proposal to just write: exceptionHandler(toCheck.value, 'Possibly unhandled rejection'); sounds nice but we pass other data types through |
|||||||||||
} else { | |||||||||||
exceptionHandler(errorMessage); | |||||||||||
} | |||||||||||
} | |||||||||||
} | |||||||||||
} | |||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -181,16 +181,14 @@ describe('q', function() { | |
}; | ||
|
||
|
||
function exceptionHandler(reason) { | ||
exceptionHandlerCalls.push(reason); | ||
} | ||
|
||
|
||
function exceptionHandlerStr() { | ||
return exceptionHandlerCalls.join('; '); | ||
function exceptionHandler(exception, reason) { | ||
if (typeof reason === 'undefined') { | ||
exceptionHandlerCalls.push({ reason: exception }); | ||
} else { | ||
exceptionHandlerCalls.push({ reason: reason, exception: exception }); | ||
} | ||
} | ||
|
||
|
||
beforeEach(function() { | ||
q = qFactory(mockNextTick.nextTick, exceptionHandler, true); | ||
q_no_error = qFactory(mockNextTick.nextTick, exceptionHandler, false); | ||
|
@@ -2167,45 +2165,98 @@ describe('q', function() { | |
|
||
|
||
describe('when exceptionHandler is called', function() { | ||
it('should log an unhandled rejected promise', function() { | ||
var defer = q.defer(); | ||
defer.reject('foo'); | ||
mockNextTick.flush(); | ||
expect(exceptionHandlerStr()).toBe('Possibly unhandled rejection: foo'); | ||
}); | ||
function CustomError() { } | ||
CustomError.prototype = Object.create(Error.prototype); | ||
|
||
var errorEg = new Error('Fail'); | ||
var errorStr = toDebugString(errorEg); | ||
|
||
it('should not log an unhandled rejected promise if disabled', function() { | ||
var defer = q_no_error.defer(); | ||
defer.reject('foo'); | ||
expect(exceptionHandlerStr()).toBe(''); | ||
}); | ||
var customError = new CustomError('Custom'); | ||
var customErrorStr = toDebugString(customError); | ||
|
||
var nonErrorObj = { isATest: 'this is' }; | ||
var nonErrorObjStr = toDebugString(nonErrorObj); | ||
|
||
it('should log a handled rejected promise on a promise without rejection callbacks', function() { | ||
var defer = q.defer(); | ||
defer.promise.then(noop); | ||
defer.reject('foo'); | ||
mockNextTick.flush(); | ||
expect(exceptionHandlerStr()).toBe('Possibly unhandled rejection: foo'); | ||
}); | ||
var fixtures = [ | ||
{ | ||
type: 'Error object', | ||
value: errorEg, | ||
expected: { | ||
exception: errorEg, | ||
reason: 'Possibly unhandled rejection: ' + errorStr | ||
} | ||
}, | ||
{ | ||
type: 'custom Error object', | ||
value: customError, | ||
expected: { | ||
exception: customError, | ||
reason: 'Possibly unhandled rejection: ' + customErrorStr | ||
} | ||
}, | ||
{ | ||
type: 'non-Error object', | ||
value: nonErrorObj, | ||
expected: { | ||
reason: 'Possibly unhandled rejection: ' + nonErrorObjStr | ||
} | ||
}, | ||
{ | ||
type: 'string primitive', | ||
value: 'foo', | ||
expected: { | ||
reason: 'Possibly unhandled rejection: foo' | ||
} | ||
} | ||
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. A couple more fixtures would be in order imo. E.g.:
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. Yeah makes sense |
||
]; | ||
forEach(fixtures, function(fixture) { | ||
var type = fixture.type; | ||
var value = fixture.value; | ||
var expected = fixture.expected; | ||
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. Extra points for putting some space before 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. Or |
||
|
||
describe('with ' + type, function() { | ||
|
||
it('should not log a handled rejected promise', function() { | ||
var defer = q.defer(); | ||
defer.promise.catch(noop); | ||
defer.reject('foo'); | ||
mockNextTick.flush(); | ||
expect(exceptionHandlerStr()).toBe(''); | ||
}); | ||
it('should log an unhandled rejected promise', function() { | ||
var defer = q.defer(); | ||
defer.reject(value); | ||
mockNextTick.flush(); | ||
expect(exceptionHandlerCalls).toEqual([expected]); | ||
}); | ||
|
||
|
||
it('should not log a handled rejected promise that is handled in a future tick', function() { | ||
var defer = q.defer(); | ||
defer.promise.catch(noop); | ||
defer.resolve(q.reject('foo')); | ||
mockNextTick.flush(); | ||
expect(exceptionHandlerStr()).toBe(''); | ||
it('should not log an unhandled rejected promise if disabled', function() { | ||
var defer = q_no_error.defer(); | ||
defer.reject(value); | ||
expect(exceptionHandlerCalls).toEqual([]); | ||
}); | ||
|
||
|
||
it('should log a handled rejected promise on a promise without rejection callbacks', function() { | ||
var defer = q.defer(); | ||
defer.promise.then(noop); | ||
defer.reject(value); | ||
mockNextTick.flush(); | ||
expect(exceptionHandlerCalls).toEqual([expected]); | ||
}); | ||
|
||
|
||
it('should not log a handled rejected promise', function() { | ||
var defer = q.defer(); | ||
defer.promise.catch(noop); | ||
defer.reject(value); | ||
mockNextTick.flush(); | ||
expect(exceptionHandlerCalls).toEqual([]); | ||
}); | ||
|
||
|
||
it('should not log a handled rejected promise that is handled in a future tick', function() { | ||
var defer = q.defer(); | ||
defer.promise.catch(noop); | ||
defer.resolve(q.reject(value)); | ||
mockNextTick.flush(); | ||
expect(exceptionHandlerCalls).toEqual([]); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
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.
Here is another idea (either for
toDebugString()
or just here):If
toCheck.value
has atoString()
method that is not the defaultObject.prototype.toString()
, then use that for serializing it. Something similar to stringify.