From 2fcd2bd2f2ad976a2809fd5d772f07ce686207ff Mon Sep 17 00:00:00 2001 From: santusht06 <115890693+santusht06@users.noreply.github.com> Date: Sun, 6 Sep 2026 15:25:59 +0530 Subject: [PATCH] process: expose enhanced stack trace to uncaughtException handlers When an EventEmitter emits an unhandled 'error' event, V8 appends an 'Emitted \'error\' event at:' frame to the stack trace via the inspector stack trace enhancer. However, this enhancement was applied only during the default fatal-exception path (inside fatalException()), after the uncaughtException and uncaughtExceptionMonitor handlers had already been invoked, so user-registered handlers never saw the enriched stack. Fix this by calling fatalExceptionStackEnhancers.beforeInspector() in createOnGlobalUncaughtException() before dispatching to the handlers. Fixes: https://github.com/nodejs/node/issues/55838 Assisted-by: Antigravity Signed-off-by: santusht06 <115890693+santusht06@users.noreply.github.com> --- lib/internal/process/execution.js | 5 ++ ...ocess-uncaught-exception-enhanced-stack.js | 47 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 test/parallel/test-process-uncaught-exception-enhanced-stack.js diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index 46cf9f95407a..c939e3842937 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -17,6 +17,7 @@ const { ERR_INVALID_ARG_TYPE, ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET, }, + fatalExceptionStackEnhancers, } = require('internal/errors'); const { validateFunction } = require('internal/validators'); const { pathToFileURL } = require('internal/url'); @@ -174,6 +175,10 @@ function createOnGlobalUncaughtException() { // call that threw and was never cleared. So clear it now. clearDefaultTriggerAsyncId(); + if (er != null && typeof er === 'object') { + fatalExceptionStackEnhancers.beforeInspector(er); + } + const type = fromPromise ? 'unhandledRejection' : 'uncaughtException'; process.emit('uncaughtExceptionMonitor', er, type); // Primary callback (e.g., domain) has priority and always handles the exception diff --git a/test/parallel/test-process-uncaught-exception-enhanced-stack.js b/test/parallel/test-process-uncaught-exception-enhanced-stack.js new file mode 100644 index 000000000000..c45ba9cb0a78 --- /dev/null +++ b/test/parallel/test-process-uncaught-exception-enhanced-stack.js @@ -0,0 +1,47 @@ +'use strict'; +const common = require('../common'); +const assert = require('node:assert'); +const EventEmitter = require('node:events'); + +class CustomEmitter extends EventEmitter {} + +const ee = new EventEmitter(); +const customEE = new CustomEmitter(); + +let monitorCount = 0; +let uncaughtCount = 0; + +process.on('uncaughtExceptionMonitor', common.mustCall((err, origin) => { + assert.strictEqual(origin, 'uncaughtException'); + monitorCount++; + if (monitorCount === 1) { + assert.match(err.stack, /Emitted 'error' event at:/); + assert.match(err.stack, /at emitPlainError/); + } else if (monitorCount === 2) { + assert.match(err.stack, /Emitted 'error' event on CustomEmitter instance at:/); + assert.match(err.stack, /at emitCustomClassError/); + } +}, 2)); + +process.on('uncaughtException', common.mustCall((err, origin) => { + assert.strictEqual(origin, 'uncaughtException'); + uncaughtCount++; + if (uncaughtCount === 1) { + assert.match(err.stack, /Emitted 'error' event at:/); + assert.match(err.stack, /at emitPlainError/); + process.nextTick(emitCustomClassError); + } else if (uncaughtCount === 2) { + assert.match(err.stack, /Emitted 'error' event on CustomEmitter instance at:/); + assert.match(err.stack, /at emitCustomClassError/); + } +}, 2)); + +function emitPlainError() { + ee.emit('error', new Error('plain error')); +} + +function emitCustomClassError() { + customEE.emit('error', new Error('custom class error')); +} + +emitPlainError();