Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Promises/better unhandled rejection message #17158

Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions lib/internal/process/promises.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
'use strict';

const { safeToString } = process.binding('util');

const promiseRejectEvent = process._promiseRejectEvent;
const hasBeenNotifiedProperty = new WeakMap();
const promiseToGuidProperty = new WeakMap();
Expand Down Expand Up @@ -60,11 +58,23 @@ function setupPromises(scheduleMicrotasks) {
}

function emitWarning(uid, reason) {
try {
if (reason instanceof Error) {
process.emitWarning(reason.stack, 'UnhandledPromiseRejectionWarning');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I guess the only downfall to doing it this way is it won't include any additional properties added on to the Error object. Any reason to do this vs using util.inspect()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just that I wasn't aware of util.inspect(). I went with what the rest of the code did in the same function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namely, Node doesn't show this information for synchronous errors:

// test.js
var e = new Error("error");
e.foo = "bar";
throw e;
~/test/test.js:3
throw e;
^

Error: error
    at Object.<anonymous> (/Users/benjamin/Downloads/test.js:1:71)
    at Module._compile (module.js:573:30)
    at Object.Module._extensions..js (module.js:584:10)
    at Module.load (module.js:507:32)
    at tryModuleLoad (module.js:470:12)
    at Function.Module._load (module.js:462:3)
    at Function.Module.runMain (module.js:609:10)
    at startup (bootstrap_node.js:158:16)
    at bootstrap_node.js:598:3

} else {
process.emitWarning(reason, 'UnhandledPromiseRejectionWarning');
Copy link
Contributor

@refack refack Nov 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this still should be safeToString(reason), notice that emitWarning will only emit strings

D:\code\4node$ promise-warning.exe\node.exe -e "Promise.reject('a');Promise.reject(100)"
(node:18696) UnhandledPromiseRejectionWarning: a
(node:18696) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:18696) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
(node:18696) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)

P.S. could you add a test for a non string rejection?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the expected behavior when strings get rejected? I couldn't find a way to get the stack trace in that case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd assume the stack has been destroyed when this is reached. IMHO just printing the reason (after stringifying) is the best we can do.
So if it's an Error reason.stack else safeToString(reason).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that test-promises-warning-on-unhandled-rejection.js implicitly checks for string rejections, please tell me if something else might be better.

I've pushed the commit to wrap the reason with safeToString().

Copy link
Contributor

@refack refack Nov 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's an awkward test... You could add a line just before last:

const p = Promise.reject(100);

then move case (2) to (3) add a case (2):

case 2:
      assert.strictEqual(warning.name, 'UnhandledPromiseRejectionWarning');
      assert(/100/.test(warning.message));
      break;

P.S. it's testing Strings because it checks the stringified output

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, should be p1 since p is used for the async handling check

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@refack Added the requested test, also added comments to make it clear what each state in the state machine means.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

}
} catch (e) {
// ignored
}

const warning = new Error(
`Unhandled promise rejection (rejection id: ${uid}): ` +
safeToString(reason));
'Unhandled promise rejection. This error originated either by ' +
'throwing inside of an async function without a catch block, ' +
'or by rejecting a promise which was not handled with .catch(). ' +
`(rejection id: ${uid})`
);
warning.name = 'UnhandledPromiseRejectionWarning';
warning.id = uid;
try {
if (reason instanceof Error) {
warning.stack = reason.stack;
Expand Down
18 changes: 18 additions & 0 deletions test/message/unhandled_promise_trace_warnings.out
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
(node:*) UnhandledPromiseRejectionWarning: Error: This was rejected
at * (*test*message*unhandled_promise_trace_warnings.js:*)
at *
at *
at *
at *
at *
at *
at *
at *
at *
at *
at *
at *
at *
at *
at *
at *
(node:*) Error: This was rejected
at * (*test*message*unhandled_promise_trace_warnings.js:*)
at *
Expand Down
7 changes: 5 additions & 2 deletions test/parallel/test-promises-unhandled-proxy-rejections.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ const expectedDeprecationWarning = 'Unhandled promise rejections are ' +
'rejections that are not handled will ' +
'terminate the Node.js process with a ' +
'non-zero exit code.';
const expectedPromiseWarning = 'Unhandled promise rejection (rejection id: ' +
'1): [object Object]';
const expectedPromiseWarning = 'Unhandled promise rejection. ' +
'This error originated either by throwing ' +
'inside of an async function without a catch ' +
'block, or by rejecting a promise which was ' +
'not handled with .catch(). (rejection id: 1)';

function throwErr() {
throw new Error('Error from proxy');
Expand Down
7 changes: 5 additions & 2 deletions test/parallel/test-promises-unhandled-symbol-rejections.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ const expectedDeprecationWarning = 'Unhandled promise rejections are ' +
'rejections that are not handled will ' +
'terminate the Node.js process with a ' +
'non-zero exit code.';
const expectedPromiseWarning = 'Unhandled promise rejection (rejection id: ' +
'1): Symbol()';
const expectedPromiseWarning = 'Unhandled promise rejection. ' +
'This error originated either by throwing ' +
'inside of an async function without a catch ' +
'block, or by rejecting a promise which was ' +
'not handled with .catch(). (rejection id: 1)';

common.expectWarning({
DeprecationWarning: expectedDeprecationWarning,
Expand Down
11 changes: 7 additions & 4 deletions test/parallel/test-promises-warning-on-unhandled-rejection.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,21 @@ let b = 0;
process.on('warning', common.mustCall((warning) => {
switch (b++) {
case 0:
assert.strictEqual(warning.name, 'UnhandledPromiseRejectionWarning');
assert(/Unhandled promise rejection/.test(warning.message));
assert.strictEqual(warning.message, 'This was rejected');
break;
case 1:
assert.strictEqual(warning.name, 'DeprecationWarning');
assert.strictEqual(warning.name, 'UnhandledPromiseRejectionWarning');
assert(/Unhandled promise rejection/.test(warning.message), 'Expected warning message to contain "Unhandled promise rejection" but did not. Had "' + warning.message + '" instead.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these lines need to be line-wrapped at <= 80 chars

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come make test's linter passes for me? I totally missed that.

break;
case 2:
assert.strictEqual(warning.name, 'DeprecationWarning');
break;
case 3:
assert.strictEqual(warning.name, 'PromiseRejectionHandledWarning');
assert(/Promise rejection was handled asynchronously/
.test(warning.message));
}
}, 3));
}, 4));

const p = Promise.reject('This was rejected');
setImmediate(common.mustCall(() => p.catch(() => {})));