Title: Debugging Typescript errors in Bazel
Slug: debugging-typescript-errors-in-bazel
Date: 2024-01-21 17:05:00
Author: Kartones
Lang: en
Tags: Development, Bazel, Typescript
og_image:
Description: An explanation of how to better debug Typescript errors when using Bazel as the build system.


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](https://github.com/Kartones/bazel-web-template)):

```bash
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:

```bash
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](https://bazel.build/docs/sandboxing#debugging-build-failures):

```bash
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.

```bash
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](https://www.typescriptlang.org/tsconfig#traceResolution) so that `tsc` emits more details.

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

```bash
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...

```bash
======== 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.