Skip to content

Commit f4d7f04

Browse files
legendecasrichardlau
authored andcommitted
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: #50827Fixes: #50733 Reviewed-By: Ethan Arrowood <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]>
1 parent e08649c commit f4d7f04

File tree

11 files changed

+148
-56
lines changed

11 files changed

+148
-56
lines changed

‎doc/api/cli.md‎

Lines changed: 13 additions & 2 deletions
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+
constoriginalPrepareStackTrace=Error.prepareStackTrace;
599+
Error.prepareStackTrace= (error, trace) =>{
600+
// Modify error and trace and format stack trace with
601+
// original Error.prepareStackTrace.
602+
returnoriginalPrepareStackTrace(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‎

Lines changed: 1 addition & 1 deletion
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‎

Lines changed: 11 additions & 2 deletions
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‎

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -85,21 +85,14 @@ const kTypes = [
8585

8686
constMainContextError=Error;
8787
constoverrideStackTrace=newSafeWeakMap();
88-
constkNoOverride=Symbol('kNoOverride');
89-
90-
constprepareStackTrace=(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-
constf=overrideStackTrace.get(error);
95-
overrideStackTrace.delete(error);
96-
returnf(error,trace);
97-
}
98-
99-
constglobalOverride=
100-
maybeOverridePrepareStackTrace(globalThis,error,trace);
101-
if(globalOverride!==kNoOverride)returnglobalOverride;
88+
letinternalPrepareStackTrace=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+
functiondefaultPrepareStackTrace(error,trace){
10396
// Normal error formatting:
10497
//
10598
// Error: Message
@@ -115,9 +108,35 @@ const prepareStackTrace = (globalThis, error, trace) =>{
115108
returnerrorString;
116109
}
117110
return`${errorString}\n at ${ArrayPrototypeJoin(trace,'\n at ')}`;
118-
};
111+
}
112+
113+
functionsetInternalPrepareStackTrace(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+
functionprepareStackTraceCallback(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+
constf=overrideStackTrace.get(error);
136+
overrideStackTrace.delete(error);
137+
returnf(error,trace);
138+
}
119139

120-
constmaybeOverridePrepareStackTrace=(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
returnMainContextError.prepareStackTrace(error,trace);
133152
}
134153

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

138166
constaggregateTwoErrors=(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‎

Lines changed: 5 additions & 21 deletions
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-
constprepareStackTrace=(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-
constf=overrideStackTrace.get(error);
38-
overrideStackTrace.delete(error);
39-
returnf(error,trace);
40-
}
41-
42-
constglobalOverride=
43-
maybeOverridePrepareStackTrace(globalThis,error,trace);
44-
if(globalOverride!==kNoOverride)returnglobalOverride;
45-
29+
functionprepareStackTraceWithSourceMaps(error,trace){
4630
leterrorString;
4731
if(kIsNodeErrorinerror){
4832
errorString=`${error.name} [${error.code}]: ${error.message}`;
@@ -57,7 +41,7 @@ const prepareStackTrace = (globalThis, error, trace) =>{
5741
letlastSourceMap;
5842
letlastFileName;
5943
constpreparedTrace=ArrayPrototypeJoin(ArrayPrototypeMap(trace,(t,i)=>{
60-
conststr=i!==0 ? '\n at ' : '';
44+
conststr='\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‎

Lines changed: 7 additions & 5 deletions
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
}elseif(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‎

Lines changed: 2 additions & 2 deletions
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(typeofMainContextError.prepareStackTrace==='function'){
693694
returnMainContextError.prepareStackTrace(error,frames);
694695
}
695-
ArrayPrototypeUnshift(frames,error);
696-
returnArrayPrototypeJoin(frames,'\n at ');
696+
returnErrorPrepareStackTrace(error,frames);
697697
});
698698
decorateErrorStack(e);
699699

Lines changed: 34 additions & 0 deletions
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+
constassert=require('assert');
6+
Error.stackTraceLimit=5;
7+
8+
assert.strictEqual(typeofError.prepareStackTrace,'function');
9+
constdefaultPrepareStackTrace=Error.prepareStackTrace;
10+
Error.prepareStackTrace=(error,trace)=>{
11+
trace=trace.filter(it=>{
12+
returnit.getFunctionName()!=='functionC';
13+
});
14+
returndefaultPrepareStackTrace(error,trace);
15+
};
16+
17+
try{
18+
require('../enclosing-call-site-min.js');
19+
}catch(e){
20+
console.log(e);
21+
}
22+
23+
deleterequire.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+
}
Lines changed: 10 additions & 0 deletions
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‎

Lines changed: 14 additions & 2 deletions
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
constassert=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(typeofError.prepareStackTrace,'function');
9+
10+
// Error.prepareStackTrace() can be overridden.
811
{
912
letprepareCalled=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+
}

0 commit comments

Comments
(0)