Skip to content

Commit f4d7f04

Browse files
legendecasrichardlau
authored andcommittedMar 25, 2024
lib: expose default prepareStackTrace
Expose the default prepareStackTrace implementation as `Error.prepareStackTrace` so that userland can chain up formatting of stack traces with built-in source maps support. PR-URL: #50827 Fixes: #50733 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
1 parent e08649c commit f4d7f04

11 files changed

+148
-56
lines changed
 

‎doc/api/cli.md

+13-2
Original file line numberDiff line numberDiff line change
@@ -589,8 +589,19 @@ application reference the transpiled code, not the original source position.
589589
`--enable-source-maps` enables caching of Source Maps and makes a best
590590
effort to report stack traces relative to the original source file.
591591

592-
Overriding `Error.prepareStackTrace` prevents `--enable-source-maps` from
593-
modifying the stack trace.
592+
Overriding `Error.prepareStackTrace` may prevent `--enable-source-maps` from
593+
modifying the stack trace. Call and return the results of the original
594+
`Error.prepareStackTrace` in the overriding function to modify the stack trace
595+
with source maps.
596+
597+
```js
598+
const originalPrepareStackTrace = Error.prepareStackTrace;
599+
Error.prepareStackTrace = (error, trace) => {
600+
// Modify error and trace and format stack trace with
601+
// original Error.prepareStackTrace.
602+
return originalPrepareStackTrace(error, trace);
603+
};
604+
```
594605

595606
Note, enabling source maps can introduce latency to your application
596607
when `Error.stack` is accessed. If you access `Error.stack` frequently

‎lib/.eslintrc.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ rules:
2323
message: Use an error exported by the internal/errors module.
2424
- selector: CallExpression[callee.object.name='Error'][callee.property.name='captureStackTrace']
2525
message: Please use `require('internal/errors').hideStackFrames()` instead.
26-
- selector: AssignmentExpression:matches([left.name='prepareStackTrace'], [left.property.name='prepareStackTrace'])
26+
- selector: AssignmentExpression:matches([left.object.name='Error']):matches([left.name='prepareStackTrace'], [left.property.name='prepareStackTrace'])
2727
message: Use 'overrideStackTrace' from 'lib/internal/errors.js' instead of 'Error.prepareStackTrace'.
2828
- selector: ThrowStatement > NewExpression[callee.name=/^ERR_[A-Z_]+$/] > ObjectExpression:first-child:not(:has([key.name='message']):has([key.name='code']):has([key.name='syscall']))
2929
message: The context passed into SystemError constructor must have .code, .syscall and .message.

‎lib/internal/bootstrap/realm.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -445,17 +445,26 @@ function setupPrepareStackTrace() {
445445
setPrepareStackTraceCallback,
446446
} = internalBinding('errors');
447447
const {
448-
prepareStackTrace,
448+
prepareStackTraceCallback,
449+
ErrorPrepareStackTrace,
449450
fatalExceptionStackEnhancers: {
450451
beforeInspector,
451452
afterInspector,
452453
},
453454
} = requireBuiltin('internal/errors');
454455
// Tell our PrepareStackTraceCallback passed to the V8 API
455456
// to call prepareStackTrace().
456-
setPrepareStackTraceCallback(prepareStackTrace);
457+
setPrepareStackTraceCallback(prepareStackTraceCallback);
457458
// Set the function used to enhance the error stack for printing
458459
setEnhanceStackForFatalException(beforeInspector, afterInspector);
460+
// Setup the default Error.prepareStackTrace.
461+
ObjectDefineProperty(Error, 'prepareStackTrace', {
462+
__proto__: null,
463+
writable: true,
464+
enumerable: false,
465+
configurable: true,
466+
value: ErrorPrepareStackTrace,
467+
});
459468
}
460469

461470
// Store the internal loaders in C++.

‎lib/internal/errors.js

+50-21
Original file line numberDiff line numberDiff line change
@@ -85,21 +85,14 @@ const kTypes = [
8585

8686
const MainContextError = Error;
8787
const overrideStackTrace = new SafeWeakMap();
88-
const kNoOverride = Symbol('kNoOverride');
89-
90-
const prepareStackTrace = (globalThis, error, trace) => {
91-
// API for node internals to override error stack formatting
92-
// without interfering with userland code.
93-
if (overrideStackTrace.has(error)) {
94-
const f = overrideStackTrace.get(error);
95-
overrideStackTrace.delete(error);
96-
return f(error, trace);
97-
}
98-
99-
const globalOverride =
100-
maybeOverridePrepareStackTrace(globalThis, error, trace);
101-
if (globalOverride !== kNoOverride) return globalOverride;
88+
let internalPrepareStackTrace = defaultPrepareStackTrace;
10289

90+
/**
91+
* The default implementation of `Error.prepareStackTrace` with simple
92+
* concatenation of stack frames.
93+
* Read more about `Error.prepareStackTrace` at https://v8.dev/docs/stack-trace-api#customizing-stack-traces.
94+
*/
95+
function defaultPrepareStackTrace(error, trace) {
10396
// Normal error formatting:
10497
//
10598
// Error: Message
@@ -115,9 +108,35 @@ const prepareStackTrace = (globalThis, error, trace) => {
115108
return errorString;
116109
}
117110
return `${errorString}\n at ${ArrayPrototypeJoin(trace, '\n at ')}`;
118-
};
111+
}
112+
113+
function setInternalPrepareStackTrace(callback) {
114+
internalPrepareStackTrace = callback;
115+
}
116+
117+
/**
118+
* Every realm has its own prepareStackTraceCallback. When `error.stack` is
119+
* accessed, if the error is created in a shadow realm, the shadow realm's
120+
* prepareStackTraceCallback is invoked. Otherwise, the principal realm's
121+
* prepareStackTraceCallback is invoked. Note that accessing `error.stack`
122+
* of error objects created in a VM Context will always invoke the
123+
* prepareStackTraceCallback of the principal realm.
124+
* @param {object} globalThis The global object of the realm that the error was
125+
* created in. When the error object is created in a VM Context, this is the
126+
* global object of that VM Context.
127+
* @param {object} error The error object.
128+
* @param {CallSite[]} trace An array of CallSite objects, read more at https://v8.dev/docs/stack-trace-api#customizing-stack-traces.
129+
* @returns {string}
130+
*/
131+
function prepareStackTraceCallback(globalThis, error, trace) {
132+
// API for node internals to override error stack formatting
133+
// without interfering with userland code.
134+
if (overrideStackTrace.has(error)) {
135+
const f = overrideStackTrace.get(error);
136+
overrideStackTrace.delete(error);
137+
return f(error, trace);
138+
}
119139

120-
const maybeOverridePrepareStackTrace = (globalThis, error, trace) => {
121140
// Polyfill of V8's Error.prepareStackTrace API.
122141
// https://crbug.com/v8/7848
123142
// `globalThis` is the global that contains the constructor which
@@ -132,8 +151,17 @@ const maybeOverridePrepareStackTrace = (globalThis, error, trace) => {
132151
return MainContextError.prepareStackTrace(error, trace);
133152
}
134153

135-
return kNoOverride;
136-
};
154+
// If the Error.prepareStackTrace was not a function, fallback to the
155+
// internal implementation.
156+
return internalPrepareStackTrace(error, trace);
157+
}
158+
159+
/**
160+
* The default Error.prepareStackTrace implementation.
161+
*/
162+
function ErrorPrepareStackTrace(error, trace) {
163+
return internalPrepareStackTrace(error, trace);
164+
}
137165

138166
const aggregateTwoErrors = (innerError, outerError) => {
139167
if (innerError && outerError && innerError !== outerError) {
@@ -1055,10 +1083,11 @@ module.exports = {
10551083
isStackOverflowError,
10561084
kEnhanceStackBeforeInspector,
10571085
kIsNodeError,
1058-
kNoOverride,
1059-
maybeOverridePrepareStackTrace,
1086+
defaultPrepareStackTrace,
1087+
setInternalPrepareStackTrace,
10601088
overrideStackTrace,
1061-
prepareStackTrace,
1089+
prepareStackTraceCallback,
1090+
ErrorPrepareStackTrace,
10621091
setArrowMessage,
10631092
SystemError,
10641093
uvErrmapGet,

‎lib/internal/source_map/prepare_stack_trace.js

+5-21
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,14 @@ const { getStringWidth } = require('internal/util/inspect');
1919
const { readFileSync } = require('fs');
2020
const { findSourceMap } = require('internal/source_map/source_map_cache');
2121
const {
22-
kNoOverride,
23-
overrideStackTrace,
24-
maybeOverridePrepareStackTrace,
2522
kIsNodeError,
2623
} = require('internal/errors');
2724
const { fileURLToPath } = require('internal/url');
2825
const { setGetSourceMapErrorSource } = internalBinding('errors');
2926

3027
// Create a prettified stacktrace, inserting context from source maps
3128
// if possible.
32-
const prepareStackTrace = (globalThis, error, trace) => {
33-
// API for node internals to override error stack formatting
34-
// without interfering with userland code.
35-
// TODO(bcoe): add support for source-maps to repl.
36-
if (overrideStackTrace.has(error)) {
37-
const f = overrideStackTrace.get(error);
38-
overrideStackTrace.delete(error);
39-
return f(error, trace);
40-
}
41-
42-
const globalOverride =
43-
maybeOverridePrepareStackTrace(globalThis, error, trace);
44-
if (globalOverride !== kNoOverride) return globalOverride;
45-
29+
function prepareStackTraceWithSourceMaps(error, trace) {
4630
let errorString;
4731
if (kIsNodeError in error) {
4832
errorString = `${error.name} [${error.code}]: ${error.message}`;
@@ -57,7 +41,7 @@ const prepareStackTrace = (globalThis, error, trace) => {
5741
let lastSourceMap;
5842
let lastFileName;
5943
const preparedTrace = ArrayPrototypeJoin(ArrayPrototypeMap(trace, (t, i) => {
60-
const str = i !== 0 ? '\n at ' : '';
44+
const str = '\n at ';
6145
try {
6246
// A stack trace will often have several call sites in a row within the
6347
// same file, cache the source map and file content accordingly:
@@ -106,8 +90,8 @@ const prepareStackTrace = (globalThis, error, trace) => {
10690
}
10791
return `${str}${t}`;
10892
}), '');
109-
return `${errorString}\n at ${preparedTrace}`;
110-
};
93+
return `${errorString}${preparedTrace}`;
94+
}
11195

11296
// Transpilers may have removed the original symbol name used in the stack
11397
// trace, if possible restore it from the names field of the source map:
@@ -210,5 +194,5 @@ function getSourceMapErrorSource(fileName, lineNumber, columnNumber) {
210194
setGetSourceMapErrorSource(getSourceMapErrorSource);
211195

212196
module.exports = {
213-
prepareStackTrace,
197+
prepareStackTraceWithSourceMaps,
214198
};

‎lib/internal/source_map/source_map_cache.js

+7-5
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
1919
const { validateBoolean } = require('internal/validators');
2020
const {
2121
setSourceMapsEnabled: setSourceMapsNative,
22-
setPrepareStackTraceCallback,
2322
} = internalBinding('errors');
23+
const {
24+
setInternalPrepareStackTrace,
25+
} = require('internal/errors');
2426
const { getLazy } = require('internal/util');
2527

2628
// Since the CJS module cache is mutable, which leads to memory leaks when
@@ -56,15 +58,15 @@ function setSourceMapsEnabled(val) {
5658
setSourceMapsNative(val);
5759
if (val) {
5860
const {
59-
prepareStackTrace,
61+
prepareStackTraceWithSourceMaps,
6062
} = require('internal/source_map/prepare_stack_trace');
61-
setPrepareStackTraceCallback(prepareStackTrace);
63+
setInternalPrepareStackTrace(prepareStackTraceWithSourceMaps);
6264
} else if (sourceMapsEnabled !== undefined) {
6365
// Reset prepare stack trace callback only when disabling source maps.
6466
const {
65-
prepareStackTrace,
67+
defaultPrepareStackTrace,
6668
} = require('internal/errors');
67-
setPrepareStackTraceCallback(prepareStackTrace);
69+
setInternalPrepareStackTrace(defaultPrepareStackTrace);
6870
}
6971

7072
sourceMapsEnabled = val;

‎lib/repl.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ const {
151151
},
152152
isErrorStackTraceLimitWritable,
153153
overrideStackTrace,
154+
ErrorPrepareStackTrace,
154155
} = require('internal/errors');
155156
const { sendInspectorCommand } = require('internal/util/inspector');
156157
const { getOptionValue } = require('internal/options');
@@ -692,8 +693,7 @@ function REPLServer(prompt,
692693
if (typeof MainContextError.prepareStackTrace === 'function') {
693694
return MainContextError.prepareStackTrace(error, frames);
694695
}
695-
ArrayPrototypeUnshift(frames, error);
696-
return ArrayPrototypeJoin(frames, '\n at ');
696+
return ErrorPrepareStackTrace(error, frames);
697697
});
698698
decorateErrorStack(e);
699699

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Flags: --enable-source-maps
2+
3+
'use strict';
4+
require('../../../common');
5+
const assert = require('assert');
6+
Error.stackTraceLimit = 5;
7+
8+
assert.strictEqual(typeof Error.prepareStackTrace, 'function');
9+
const defaultPrepareStackTrace = Error.prepareStackTrace;
10+
Error.prepareStackTrace = (error, trace) => {
11+
trace = trace.filter(it => {
12+
return it.getFunctionName() !== 'functionC';
13+
});
14+
return defaultPrepareStackTrace(error, trace);
15+
};
16+
17+
try {
18+
require('../enclosing-call-site-min.js');
19+
} catch (e) {
20+
console.log(e);
21+
}
22+
23+
delete require.cache[require
24+
.resolve('../enclosing-call-site-min.js')];
25+
26+
// Disable
27+
process.setSourceMapsEnabled(false);
28+
assert.strictEqual(process.sourceMapsEnabled, false);
29+
30+
try {
31+
require('../enclosing-call-site-min.js');
32+
} catch (e) {
33+
console.log(e);
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Error: an error!
2+
at functionD (*enclosing-call-site.js:16:17)
3+
at functionB (*enclosing-call-site.js:6:3)
4+
at functionA (*enclosing-call-site.js:2:3)
5+
at Object.<anonymous> (*enclosing-call-site.js:24:3)
6+
Error: an error!
7+
at functionD (*enclosing-call-site-min.js:1:156)
8+
at functionB (*enclosing-call-site-min.js:1:60)
9+
at functionA (*enclosing-call-site-min.js:1:26)
10+
at Object.<anonymous> (*enclosing-call-site-min.js:1:199)

‎test/parallel/test-error-prepare-stack-trace.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
// Flags: --enable-source-maps
21
'use strict';
32

43
require('../common');
4+
const { spawnSyncAndExitWithoutError } = require('../common/child_process');
55
const assert = require('assert');
66

7-
// Error.prepareStackTrace() can be overridden with source maps enabled.
7+
// Verify that the default Error.prepareStackTrace is present.
8+
assert.strictEqual(typeof Error.prepareStackTrace, 'function');
9+
10+
// Error.prepareStackTrace() can be overridden.
811
{
912
let prepareCalled = false;
1013
Error.prepareStackTrace = (_error, trace) => {
@@ -17,3 +20,12 @@ const assert = require('assert');
1720
}
1821
assert(prepareCalled);
1922
}
23+
24+
if (process.argv[2] !== 'child') {
25+
// Verify that the above test still passes when source-maps support is
26+
// enabled.
27+
spawnSyncAndExitWithoutError(
28+
process.execPath,
29+
['--enable-source-maps', __filename, 'child'],
30+
{});
31+
}

‎test/parallel/test-node-output-sourcemaps.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ describe('sourcemaps output', { concurrency: true }, () => {
3131
{ name: 'source-map/output/source_map_enclosing_function.js' },
3232
{ name: 'source-map/output/source_map_eval.js' },
3333
{ name: 'source-map/output/source_map_no_source_file.js' },
34+
{ name: 'source-map/output/source_map_prepare_stack_trace.js' },
3435
{ name: 'source-map/output/source_map_reference_error_tabs.js' },
3536
{ name: 'source-map/output/source_map_sourcemapping_url_string.js' },
3637
{ name: 'source-map/output/source_map_throw_catch.js' },

0 commit comments

Comments
 (0)
Please sign in to comment.