Skip to content

Commit 68d508a

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 f2d9379 commit 68d508a

10 files changed

+169
-2
lines changed

lib/events.js

+54-1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,47 @@ EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
9898
return $getMaxListeners(this);
9999
};
100100

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

@@ -113,13 +154,25 @@ EventEmitter.prototype.emit = function emit(type, ...args) {
113154
if (args.length > 0)
114155
er = args[0];
115156
if (er instanceof Error) {
157+
try {
158+
const { kExpandStackSymbol } = require('internal/util');
159+
const capture = {};
160+
Error.captureStackTrace(capture, EventEmitter.prototype.emit);
161+
Object.defineProperty(er, kExpandStackSymbol, {
162+
value: enhanceStackTrace.bind(null, er, capture),
163+
configurable: true
164+
});
165+
} catch (e) {}
166+
167+
// Note: The comments on the `throw` lines are intentional, they show
168+
// up in Node's output if this results in an unhandled exception.
116169
throw er; // Unhandled 'error' event
117170
}
118171
// At least give some kind of context to the user
119172
const errors = lazyErrors();
120173
const err = new errors.Error('ERR_UNHANDLED_ERROR', er);
121174
err.context = er;
122-
throw err;
175+
throw err; // Unhandled 'error' event
123176
}
124177

125178
const handler = events[type];

lib/internal/bootstrap_node.js

+5
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,11 @@
458458
} catch (er) {
459459
// nothing to be done about it at this point.
460460
}
461+
try {
462+
const { kExpandStackSymbol } = NativeModule.require('internal/util');
463+
if (typeof er[kExpandStackSymbol] === 'function')
464+
er[kExpandStackSymbol]();
465+
} catch (er) {}
461466
return false;
462467
}
463468

lib/internal/util.js

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

395395
// Used by the buffer module to capture an internal reference to the
396396
// default isEncoding implementation, just in case userland overrides it.
397-
kIsEncodingSymbol: Symbol('node.isEncoding')
397+
kIsEncodingSymbol: Symbol('kIsEncodingSymbol'),
398+
kExpandStackSymbol: Symbol('kExpandStackSymbol')
398399
};
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)