Debugging Typescript errors in Bazel

Typescript errors are not always easy to debug. Add Bazel to the mix, and some errors can feel a nightmare to understand and resolve. Thankfully, there are two steps that will help a lot with triaging.

Let's imagine we have the following action, which transpiles some typescript and then runs it via NodeJS (you can find it at my Github bazel-web-template repo):

bazel run src/ts:run_d

This action calls a js_binary, to a transpiled d.mjs file, which imports from c.mjs. I have on purpose modified the import to wrongly ask for e.mjs, so when we run it, inside the Bazel execution output, we'll get the following error:

src/ts/d.mts(1,28): error TS2307: Cannot find module './e.mjs' or its corresponding type declarations.

Now let's keep imagining that we have no clue where could that error come from; We just know that something is trying to import e.mjs and fails to do so.

The first step to ease debugging, is to tell Bazel to keep the sandbox when the execution ends, so we can inspect and replicate what it ran. For that, we can use the sandbox_debug flag:

bazel run src/ts:run_d --sandbox_debug

Now, the output will be more verbose, and we will notice that, around where it outputs the error, there will be something referring to a failure/error executing a command, followed by a long execution line. In this example, calling a tsc wrapper script.

Note: I've simplified a bit the paths [1] and added some new lines for legibility.

linux-sandbox failed: error executing TsProject command 
(cd /bazel/sandbox/linux-sandbox/226/execroot/_main && \
exec env - \
 BAZEL_BINDIR=bazel-out/k8-fastbuild/bin \
 TMPDIR=/tmp \
/bazel/install/linux-sandbox -W /tmp/ -t 15 -w /dev/shm -w /tmp -w /tmp/
 bazel-execroot/_main 
 -M /bazel/execroot 
 -m /bazel/sandbox/linux-sandbox/226/_hermetic_tmp/bazel-execroot 
 -M /bazel/sandbox/linux-sandbox/226/execroot 
 -m /bazel/sandbox/linux-sandbox/226/_hermetic_tmp/bazel-working-directory 
 -M /bazel/external/aspect_rules_js 
 -m /bazel/sandbox/linux-sandbox/226/_hermetic_tmp/bazel-source-roots/0 
 -M /bazel/external/nodejs_linux_amd64 
 -m /bazel/sandbox/linux-sandbox/226/_hermetic_tmp/bazel-source-roots/1 
 -M /bazel/sandbox/linux-sandbox/226/_hermetic_tmp 
 -m /tmp -S /bazel/sandbox/linux-sandbox/226/stats.out 
 -N -D /bazel/sandbox/linux-sandbox/226/debug.out 
 -- bazel-out/k8-opt-exec/bin/external/npm_typescript/tsc.sh 
 --project tsconfig.json 
 --outDir src/ts 
 --rootDir src/ts 
 --declarationDir src/ts 
 --tsBuildInfoFile src/ts/d.tsbuildinfo)

If we re-execute in the command line that big command, we can replay very similarly what Bazel did: In our case, apparently some transpilation taking place at src/ts. But not only that, we can also alter it, by adding for example the traceResolution flag so that tsc emits more details.

Let's add it to the end, for example as --traceResolution >> out.txt:

cd /bazel/sandbox/linux-sandbox/226/execroot/_main && \
exec env - \
 BAZEL_BINDIR=bazel-out/k8-fastbuild/bin \
 TMPDIR=/tmp \
/bazel/install/linux-sandbox -W /tmp/ -t 15 -w /dev/shm -w /tmp -w /tmp/
 bazel-execroot/_main 
 -M /bazel/execroot 
 -m /bazel/sandbox/linux-sandbox/226/_hermetic_tmp/bazel-execroot 
 -M /bazel/sandbox/linux-sandbox/226/execroot 
 -m /bazel/sandbox/linux-sandbox/226/_hermetic_tmp/bazel-working-directory 
 -M /bazel/external/aspect_rules_js 
 -m /bazel/sandbox/linux-sandbox/226/_hermetic_tmp/bazel-source-roots/0 
 -M /bazel/external/nodejs_linux_amd64 
 -m /bazel/sandbox/linux-sandbox/226/_hermetic_tmp/bazel-source-roots/1 
 -M /bazel/sandbox/linux-sandbox/226/_hermetic_tmp 
 -m /tmp -S /bazel/sandbox/linux-sandbox/226/stats.out 
 -N -D /bazel/sandbox/linux-sandbox/226/debug.out 
 -- bazel-out/k8-opt-exec/bin/external/npm_typescript/tsc.sh 
 --project tsconfig.json 
 --outDir src/ts 
 --rootDir src/ts 
 --declarationDir src/ts 
 --tsBuildInfoFile src/ts/d.tsbuildinfo
 --traceResolution >> out.txt

Executing that command, we will get a very detailed tsc module resolution trace. I Highly recommended to output it to a file, as even with simple examples it is quite large!

Analysing the trace resolution, we search for e.mjs and something-something resolution...

======== Resolving module './e.mjs' 
 from '/tmp//bazel-out/k8-fastbuild/bin/src/ts/d.mts'. ========
Explicitly specified module resolution kind: 'NodeNext'.
Resolving in ESM mode with conditions 'import', 'types', 'node'.
Loading module as file / folder, 
 candidate module location '/tmp//bazel-out/k8-fastbuild/bin/src/ts/e.mjs', 
 target file types: TypeScript, JavaScript, Declaration, JSON.
File name '/tmp//bazel-out/k8-fastbuild/bin/src/ts/e.mjs' has a '.mjs' extension
 - stripping it.
File '/tmp//bazel-out/k8-fastbuild/bin/src/ts/e.mts' does not exist.
File '/tmp//bazel-out/k8-fastbuild/bin/src/ts/e.d.mts' does not exist.
File '/tmp//bazel-out/k8-fastbuild/bin/src/ts/e.mjs' does not exist.
Directory '/tmp//bazel-out/k8-fastbuild/bin/src/ts/e.mjs' does not exist, 
 skipping all lookups in it.
======== Module name './e.mjs' was not resolved. ========

And there it is, we see that the resolution comes from from [...] src/ts/d.mts. We can also see all the variants that it tried (TS/JS + type definitions), and if this was a potential package dependency, we would see it also crawl up, searching for node_modules folders and looking inside them if present.

As a summary, by enabling the sandbox persistence, and then re-playing the executed command with some extra flags, we can debug way better most Typescript errors.

Before wrapping up this post, I also want to mention some more generic debugging advices:

Checking bazel-out to see which files you have can be very helpful. Just remember to run a bazel clean if want to be certain that all files you're seeing are from your last execution, and not from past ones. Web rules tend to leave a lot of leftovers.

Many times, the source of the error is file visibility-related: You are transpiling the correct files, but then either you're not exposing them at the intended target (e.g. via data or as a dependency), or maybe your tsconfig.json is not properly set and as an example module resolution is not correctly configured, or you're missing some rootDir/rootDirs. About rootDirs, they are not that problematic in Bazel unless you use yarn/pnpm/etc. workspaces.

And finally, your mileage and preferences might vary, but in Bazel I prefer a classic approach of transpiling everything to Javascript, and then running binaries and tests against JS-only file sets, versus using ts-node, babel and similar under-the-hood transformations. Being able to check at each step if you have the proper files eases debugging and simplifies configuration a lot (specially during migrations).

[1] : Sandbox paths including the action tend to be quite long; A few times, long enough to cause issues under Windows.

Tags: Bazel Development Typescript

Debugging Typescript errors in Bazel article, written by Kartones. Published on