Skip to content

Commit 5b8c97f

Browse files
committed
events: show throw stack trace for uncaught exception
Show the stack trace for the `eventemitter.emit('error')` call in the case of an uncaught exception. Previously, there would be no clue in Node’s output about where the actual `throw` comes from. PR-URL: #19003 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent db8d197 commit 5b8c97f

10 files changed

+169
-2
lines changed

lib/events.js

+54-1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,47 @@ EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
9494
return $getMaxListeners(this);
9595
};
9696

97+
// Returns the longest sequence of `a` that fully appears in `b`,
98+
// of length at least 3.
99+
// This is a lazy approach but should work well enough, given that stack
100+
// frames are usually unequal or otherwise appear in groups, and that
101+
// we only run this code in case of an unhandled exception.
102+
function longestSeqContainedIn(a, b) {
103+
for (var len = a.length; len >= 3; --len) {
104+
for (var i = 0; i < a.length - len; ++i) {
105+
// Attempt to find a[i:i+len] in b
106+
for (var j = 0; j < b.length - len; ++j) {
107+
let matches = true;
108+
for (var k = 0; k < len; ++k) {
109+
if (a[i + k] !== b[j + k]) {
110+
matches = false;
111+
break;
112+
}
113+
}
114+
if (matches)
115+
return [ len, i, j ];
116+
}
117+
}
118+
}
119+
120+
return [ 0, 0, 0 ];
121+
}
122+
123+
function enhanceStackTrace(err, own) {
124+
const sep = '\nEmitted \'error\' event at:\n';
125+
126+
const errStack = err.stack.split('\n').slice(1);
127+
const ownStack = own.stack.split('\n').slice(1);
128+
129+
const [ len, off ] = longestSeqContainedIn(ownStack, errStack);
130+
if (len > 0) {
131+
ownStack.splice(off + 1, len - 1,
132+
' [... lines matching original stack trace ...]');
133+
}
134+
// Do this last, because it is the only operation with side effects.
135+
err.stack = err.stack + sep + ownStack.join('\n');
136+
}
137+
97138
EventEmitter.prototype.emit = function emit(type, ...args) {
98139
let doError = (type === 'error');
99140

@@ -109,13 +150,25 @@ EventEmitter.prototype.emit = function emit(type, ...args) {
109150
if (args.length > 0)
110151
er = args[0];
111152
if (er instanceof Error) {
153+
try {
154+
const { kExpandStackSymbol } = require('internal/util');
155+
const capture = {};
156+
Error.captureStackTrace(capture, EventEmitter.prototype.emit);
157+
Object.defineProperty(er, kExpandStackSymbol, {
158+
value: enhanceStackTrace.bind(null, er, capture),
159+
configurable: true
160+
});
161+
} catch (e) {}
162+
163+
// Note: The comments on the `throw` lines are intentional, they show
164+
// up in Node's output if this results in an unhandled exception.
112165
throw er; // Unhandled 'error' event
113166
}
114167
// At least give some kind of context to the user
115168
const errors = lazyErrors();
116169
const err = new errors.Error('ERR_UNHANDLED_ERROR', er);
117170
err.context = er;
118-
throw err;
171+
throw err; // Unhandled 'error' event
119172
}
120173

121174
const handler = events[type];

lib/internal/bootstrap_node.js

+5
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,11 @@
438438
} catch (er) {
439439
// nothing to be done about it at this point.
440440
}
441+
try {
442+
const { kExpandStackSymbol } = NativeModule.require('internal/util');
443+
if (typeof er[kExpandStackSymbol] === 'function')
444+
er[kExpandStackSymbol]();
445+
} catch (er) {}
441446
return false;
442447
}
443448

lib/internal/util.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -320,5 +320,6 @@ module.exports = {
320320

321321
// Used by the buffer module to capture an internal reference to the
322322
// default isEncoding implementation, just in case userland overrides it.
323-
kIsEncodingSymbol: Symbol('node.isEncoding')
323+
kIsEncodingSymbol: Symbol('kIsEncodingSymbol'),
324+
kExpandStackSymbol: Symbol('kExpandStackSymbol')
324325
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict';
2+
require('../common');
3+
const EventEmitter = require('events');
4+
5+
function foo() {
6+
function bar() {
7+
return new Error('foo:bar');
8+
}
9+
10+
return bar();
11+
}
12+
13+
const ee = new EventEmitter();
14+
const err = foo();
15+
16+
function quux() {
17+
ee.emit('error', err);
18+
}
19+
20+
quux();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
events.js:*
2+
throw er; // Unhandled 'error' event
3+
^
4+
5+
Error: foo:bar
6+
at bar (*events_unhandled_error_common_trace.js:*:*)
7+
at foo (*events_unhandled_error_common_trace.js:*:*)
8+
at Object.<anonymous> (*events_unhandled_error_common_trace.js:*:*)
9+
at Module._compile (module.js:*:*)
10+
at Object.Module._extensions..js (module.js:*:*)
11+
at Module.load (module.js:*:*)
12+
at tryModuleLoad (module.js:*:*)
13+
at Function.Module._load (module.js:*:*)
14+
at Function.Module.runMain (module.js:*:*)
15+
at startup (bootstrap_node.js:*:*)
16+
Emitted 'error' event at:
17+
at quux (*events_unhandled_error_common_trace.js:*:*)
18+
at Object.<anonymous> (*events_unhandled_error_common_trace.js:*:*)
19+
at Module._compile (module.js:*:*)
20+
[... lines matching original stack trace ...]
21+
at startup (bootstrap_node.js:*:*)
22+
at bootstrap_node.js:*:*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
require('../common');
3+
const EventEmitter = require('events');
4+
const er = new Error();
5+
process.nextTick(() => {
6+
new EventEmitter().emit('error', er);
7+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
events.js:*
2+
throw er; // Unhandled 'error' event
3+
^
4+
5+
Error
6+
at Object.<anonymous> (*events_unhandled_error_nexttick.js:*:*)
7+
at Module._compile (module.js:*:*)
8+
at Object.Module._extensions..js (module.js:*:*)
9+
at Module.load (module.js:*:*)
10+
at tryModuleLoad (module.js:*:*)
11+
at Function.Module._load (module.js:*:*)
12+
at Function.Module.runMain (module.js:*:*)
13+
at startup (bootstrap_node.js:*:*)
14+
at bootstrap_node.js:*:*
15+
Emitted 'error' event at:
16+
at process.nextTick (*events_unhandled_error_nexttick.js:*:*)
17+
at process._tickCallback (internal/process/next_tick.js:*:*)
18+
at Function.Module.runMain (module.js:*:*)
19+
at startup (bootstrap_node.js:*:*)
20+
at bootstrap_node.js:*:*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
'use strict';
2+
require('../common');
3+
const EventEmitter = require('events');
4+
new EventEmitter().emit('error', new Error());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
events.js:*
2+
throw er; // Unhandled 'error' event
3+
^
4+
5+
Error
6+
at Object.<anonymous> (*events_unhandled_error_sameline.js:*:*)
7+
at Module._compile (module.js:*:*)
8+
at Object.Module._extensions..js (module.js:*:*)
9+
at Module.load (module.js:*:*)
10+
at tryModuleLoad (module.js:*:*)
11+
at Function.Module._load (module.js:*:*)
12+
at Function.Module.runMain (module.js:*:*)
13+
at startup (bootstrap_node.js:*:*)
14+
at bootstrap_node.js:*:*
15+
Emitted 'error' event at:
16+
at Object.<anonymous> (*events_unhandled_error_sameline.js:*:*)
17+
at Module._compile (module.js:*:*)
18+
[... lines matching original stack trace ...]
19+
at bootstrap_node.js:*:*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const EventEmitter = require('events');
5+
6+
// Tests that the error stack where the exception was thrown is *not* appended.
7+
8+
process.on('uncaughtException', common.mustCall((err) => {
9+
const lines = err.stack.split('\n');
10+
assert.strictEqual(lines[0], 'Error');
11+
lines.slice(1).forEach((line) => {
12+
assert(/^ at/.test(line), `${line} has an unexpected format`);
13+
});
14+
}));
15+
16+
new EventEmitter().emit('error', new Error());

0 commit comments

Comments
 (0)