Stack tracing / debug dump of intermediate files
This PR adds two features to the JS Compute Runtime, both using just JavaScript means (rather than by modifying the C++).
Stack tracing (--enable-stack-traces)
Consider the following example code.
addEventListener('fetch', e => e.respondWith(handler(e)));
async function buildResult(fn: () => number): Promise<Response> {
const x = fn();
return new Response(String(x));
}
async function handler(_e: FetchEvent) {
const result = await buildResult(() => {
throw new TypeError('foo');
});
result.headers.set('content-type', 'text/plain');
return result;
}
By enabling the new option, the above code can be made to return an error stack like this:
2025-11-14T07:01:15.535095Z INFO request{id=0}: handling request GET http://localhost:7676/
Unhandled error while running request handler
TypeError: foo
at (anonymous function) (src/index.ts:10:11)
7 |
8 | async function handler(_e: FetchEvent) {
9 | const result = await buildResult(() => {
> 10 | throw new TypeError('foo');
^
11 | });
12 | result.headers.set('content-type', 'text/plain');
13 | return result;
at buildResult (src/index.ts:4:13)
at handler (src/index.ts:9:24)
at src/index.ts:1:46
Raw error below:
Error while running request handler: foo
Stack:
handler/result<@fastly:app.js:722:11
buildResult@fastly:app.js:717:13
handler@fastly:app.js:721:24
@fastly:app.js:715:48
node_modules/@fastly/js-compute/src/rsrc/trace-mapping.inject.js/globalThis.addEventListener/<@fastly:app.js:694:33
2025-11-14T07:01:15.571068Z INFO request{id=0}: response status: 500
2025-11-14T07:01:15.571332Z INFO request{id=0}: request completed using 7.7 MB of WebAssembly heap
2025-11-14T07:01:15.571339Z INFO request{id=0}: request completed in 36ms
instead of
2025-11-14T07:00:32.689743Z INFO request{id=0}: handling request GET http://localhost:7676/
Error while running request handler: foo
Stack:
handler/result<@fastly:app.js:23:11
buildResult@fastly:app.js:18:13
handler@fastly:app.js:22:24
@fastly:app.js:16:48
2025-11-14T07:00:32.692283Z INFO request{id=0}: response status: 500
2025-11-14T07:00:32.692612Z INFO request{id=0}: request completed using 7.7 MB of WebAssembly heap
2025-11-14T07:00:32.692623Z INFO request{id=0}: request completed in 3ms
This also catches errors at the top level. Consider the following top-level code:
async function init() {
throw new TypeError('foo');
}
await init();
export {};
This causes an error such as the following during the Wizer step
Unhandled error while running top level module code
TypeError: foo
at init (src/index.ts:2:9)
1 | async function init() {
> 2 | throw new TypeError('foo');
^
3 | }
4 | await init();
5 | export {};
at __fastly_init_guard__ (src/index.ts:4:7)
Raw error below:
Exception while evaluating top-level script
__fastly_bundle_with_sourcemaps.js:716:9 TypeError: foo
Additionally, some promises were rejected, but the rejection never handled:
Promise rejected but never handled: foo
Stack:
init@fastly:app.js:716:9
__fastly_init_guard__@fastly:app.js:718:7
@fastly:app.js:719:3
instead of
Exception while evaluating top-level script
__fastly_post_bundle.js:17:9 TypeError: foo
Additionally, some promises were rejected, but the rejection never handled:
Promise rejected but never handled: foo
Stack:
init@fastly:app.js:17:9
@fastly:app.js:20:7
This is done by injecting the composed source map from esbuild and magic-string, as well as a trace mapping script into the resulting bundle.
It also works with any internal or external source maps that may already exist on the input file referenced by sourceMappingURL (external or internal data-url), for example if there is an additional bundler that runs (Webpack, tsc, etc.) before js-compute-runtime.
Source code contents are only included from user code (i.e., excludes node_modules and any injected code that results from transforms), and the code dump is provided for the top-most frame that exists in user code. However, this can be removed by specifying the --exclude-sources flag, resulting in the mapped stack trace only.
This mode also makes two exported functions available from fastly:experimental:
/**
* Get information about an error as a ready-to-print array of strings.
* This includes the error name, message, and a call stack.
* If --enable-stack-traces is specified during build, the call stack
* will be mapped using source maps.
* If --enable-stack-traces is specified and --exclude-sources is not specified,
* then this will also include a code dump of neighboring lines of user code.
* @param error
*/
export function mapError(error: Error | string): (Error | string)[];
/**
* Calls mapError(error) and outputs the results to stderr output.
* @param error
*/
export function mapAndLogError(error: Error | string): void;
This makes mapped error stacks available to user error handling code, e.g.
import { mapError } from 'fastly:experimental';
addEventListener('fetch', e => e.respondWith(handler(e)));
async function handler(_e: FetchEvent) {
let result: Response;
try {
throw new TypeError('foo');
result = new Response();
} catch(err) {
return new Response(mapError(err).join('\n'));
}
return result;
}
Then curling this would give:
% curl http://localhost:7676/
TypeError: foo
at handler (src/index.ts:12:11)
9 | async function handler(_e: FetchEvent) {
10 | let result: Response;
11 | try {
> 12 | throw new TypeError('foo');
^
13 | result = new Response();
14 | } catch(err) {
15 | return new Response(mapError(err).join('\n'));
at src/index.ts:3:46
This feature adds a small amount of data to the Wasm package, but parsing the sourcemap does not occur on a hot code path, so runtime performance is not expected to be impacted strongly.
Outputting intermediate files (--debug-intermediate-files <dir>)
The bundling process has the following steps
input file
--(bundle: esbuild)--> intermediate file 1
--(postbundle: magic-string)--> intermediate file 2
--(inject sourcemap)--> final file to pass to wizer
- The bundle step uses esbuild to transform modules and apply the Fastly plugin (for example fastly:* namespaces)
- in the new mode, this also injects the stack trace mapping functionality
- The postbundle uses magic-string to inject pre-compiled regexes
- in the new mode, this also injects the initialization guard and placeholder for inserting sourcemap
- The inject sourcemap step is new and replaces a marker with the final composed sourcemap content
After package has been built, the runtime deletes all of the temporary files. However it is sometimes useful to have access to view the intermediate files. By specifying the --debug-intermediate-files <dir> flag, these intermediate files are dumped into the specified directory to aid in debugging. For example:
js-compute-runtime --debug-intermediate-files ./bin src/index.ts ./bin/main.wasm
This will output the files __1_bundled.js and __2_postbundled.js as well as fastly_bundle.js to the ./bin directory, which can be examined. If --enable-stack-traces is also set, then intermediate source map files __1_bundled.js.map, __2_postbundled.js.map, as well as the final source map file fastly_sourcemaps.json will also be emitted.
@harmony7 I think the test failures here are legit ones, can you have a look?
@tartanllama
Thanks, I have been able to fix some of the problems. However, I don't think the others relate to my changes. can you take a look and help?
- The
formatjob is giving this
> @fastly/[email protected] format:check
> prettier --check *.js src/*.js integration-tests
Checking formatting...
[warn] integration-tests/js-compute/test.js
[warn] Code style issues found in the above file. Run Prettier with --write to fix.
Error: Process completed with exit code 1.
However, on my local:
% npm run format:check
> @fastly/[email protected] format:check
> prettier --check *.js src/*.js integration-tests
Checking formatting...
All matched files use Prettier code style!
So I'm not sure how to clear this one up.
- The sdktest ones are failing on
/kv-store-e2e/listand/aclreturning 200 instead of 500 from the fixture. This are probably unrelated to my changes. can you help figure these out?
Additionally, the docusaurus one is failing due to OOM it looks like.
The code looks all reasonable to me, but @zkat would likely be a better reviewer for it as our JS build expert 😄
@TartanLlama I've applied your suggestions =) let me know if there is anything else I should do
I've submitted a fix to resolve the docusaurus errors as #1236.