From 0f493e9baf281c4c754cf48476aa3a27f91342c2 Mon Sep 17 00:00:00 2001 From: Benjamin Gruenbaum Date: Mon, 24 Jan 2022 20:47:11 +0200 Subject: [PATCH 1/2] process: unhandledRejection support more errors Support cross realm errors where `instanceof` errors in our unhandledRejection warning to print better error with stack traces. --- lib/internal/process/promises.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/internal/process/promises.js b/lib/internal/process/promises.js index 76099b1032a466..49445d258febe7 100644 --- a/lib/internal/process/promises.js +++ b/lib/internal/process/promises.js @@ -5,6 +5,7 @@ const { ArrayPrototypeShift, Error, ObjectDefineProperty, + ObjectPrototypeHasOwnProperty, SafeWeakMap, } = primordials; @@ -179,14 +180,21 @@ function emitUnhandledRejectionWarning(uid, reason) { `(rejection id: ${uid})` ); try { - if (reason instanceof Error) { + if (ObjectPrototypeHasOwnProperty(reason, 'stack')) { warning.stack = reason.stack; process.emitWarning(reason.stack, unhandledRejectionErrName); } else { process.emitWarning( noSideEffectsToString(reason), unhandledRejectionErrName); } - } catch {} + } catch { + try { + process.emitWarning( + noSideEffectsToString(reason), unhandledRejectionErrName); + } catch { + // Ignore. + } + } process.emitWarning(warning); } @@ -232,7 +240,7 @@ function processPromiseRejections() { try { switch (unhandledRejectionsMode) { case kStrictUnhandledRejections: { - const err = reason instanceof Error ? + const err = ObjectPrototypeHasOwnProperty(reason, 'stack') ? reason : generateUnhandledRejectionError(reason); // This destroys the async stack, don't clear it after triggerUncaughtException(err, true /* fromPromise */); @@ -259,7 +267,7 @@ function processPromiseRejections() { case kThrowUnhandledRejections: { const handled = emit(reason, promise, promiseInfo); if (!handled) { - const err = reason instanceof Error ? + const err = ObjectPrototypeHasOwnProperty(reason, 'stack') ? reason : generateUnhandledRejectionError(reason); // This destroys the async stack, don't clear it after triggerUncaughtException(err, true /* fromPromise */); From 9a234a525fda4d2eb44f6592287f092c33127725 Mon Sep 17 00:00:00 2001 From: Benjamin Gruenbaum Date: Tue, 25 Jan 2022 14:33:05 +0200 Subject: [PATCH 2/2] add test --- lib/internal/process/promises.js | 12 +++++++++--- test/parallel/test-promise-unhandled-warn.js | 19 +++++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/internal/process/promises.js b/lib/internal/process/promises.js index 49445d258febe7..fd5cb73fbbe4bb 100644 --- a/lib/internal/process/promises.js +++ b/lib/internal/process/promises.js @@ -80,6 +80,12 @@ function hasRejectionToWarn() { return tickInfo[kHasRejectionToWarn] === 1; } +function isErrorLike(o) { + return typeof o === 'object' && + o !== null && + ObjectPrototypeHasOwnProperty(o, 'stack'); +} + function getUnhandledRejectionsMode() { const { getOptionValue } = require('internal/options'); switch (getOptionValue('--unhandled-rejections')) { @@ -180,7 +186,7 @@ function emitUnhandledRejectionWarning(uid, reason) { `(rejection id: ${uid})` ); try { - if (ObjectPrototypeHasOwnProperty(reason, 'stack')) { + if (isErrorLike(reason)) { warning.stack = reason.stack; process.emitWarning(reason.stack, unhandledRejectionErrName); } else { @@ -240,7 +246,7 @@ function processPromiseRejections() { try { switch (unhandledRejectionsMode) { case kStrictUnhandledRejections: { - const err = ObjectPrototypeHasOwnProperty(reason, 'stack') ? + const err = isErrorLike(reason) ? reason : generateUnhandledRejectionError(reason); // This destroys the async stack, don't clear it after triggerUncaughtException(err, true /* fromPromise */); @@ -267,7 +273,7 @@ function processPromiseRejections() { case kThrowUnhandledRejections: { const handled = emit(reason, promise, promiseInfo); if (!handled) { - const err = ObjectPrototypeHasOwnProperty(reason, 'stack') ? + const err = isErrorLike(reason) ? reason : generateUnhandledRejectionError(reason); // This destroys the async stack, don't clear it after triggerUncaughtException(err, true /* fromPromise */); diff --git a/test/parallel/test-promise-unhandled-warn.js b/test/parallel/test-promise-unhandled-warn.js index aab07973be40aa..f13e6d29e2f087 100644 --- a/test/parallel/test-promise-unhandled-warn.js +++ b/test/parallel/test-promise-unhandled-warn.js @@ -2,6 +2,7 @@ 'use strict'; const common = require('../common'); +const assert = require('assert'); // Verify that ignoring unhandled rejection works fine and that no warning is // logged. @@ -12,11 +13,25 @@ new Promise(() => { Promise.reject('test'); +function lookForMeInStackTrace() { + Promise.reject(new class ErrorLike { + constructor() { + Error.captureStackTrace(this); + this.message = 'ErrorLike'; + } + }()); +} +lookForMeInStackTrace(); + // Unhandled rejections trigger two warning per rejection. One is the rejection // reason and the other is a note where this warning is coming from. -process.on('warning', common.mustCall(4)); +process.on('warning', common.mustCall((reason) => { + if (reason.message.includes('ErrorLike')) { + assert.match(reason.stack, /lookForMeInStackTrace/); + } +}, 6)); process.on('uncaughtException', common.mustNotCall('uncaughtException')); -process.on('rejectionHandled', common.mustCall(2)); +process.on('rejectionHandled', common.mustCall(3)); process.on('unhandledRejection', (reason, promise) => { // Handle promises but still warn!