Day#1: Building Clean, Breaking Things: The Unexpected Cost of a Global Exception Filter

The goal seemed trivial: create a global ExceptionFilter to unify error responses across the backend. A quick win. A small boost in DX. What followed instead was a full day of debugging TypeScript build issues, resolving unexpected behavior in ESM mode, and untangling the consequences of mixing modern monorepo tooling with legacy compiler features.

This post is a breakdown of what went wrong — and how it was ultimately fixed.

mariusz
mariusz
20 posts
2 followers

The setup

The project stack:

  • NestJS with TypeScript in ESM mode
  • Turborepo for managing multiple applications and shared libraries
  • Path aliases declared in tsconfig.base.json
  • Separate shared library for infrastructure concerns (libs/filters)
  • Testing with Jest, type checking with tsc, running via ts-node

The intention was simple: place the reusable filter in libs/filters, export it via @tin-filters/all-exceptions.filter, and import it globally in the app. Nothing revolutionary.

Until everything broke.


What went wrong

The moment the filter was imported using a path alias (@tin-filters/...), everything started to fail in different places depending on the tool used.

  • tsc --build threw TS6059 and TS6307 — complaints about files being outside rootDir and not part of the build.
  • ts-node in ESM mode crashed with ERR_MODULE_NOT_FOUND — ignoring tsconfig.paths entirely.
  • Jest threw ts-jest warnings and runtime errors — trying to transform .js files without allowJs.

All because of one decision: combining TypeScript path aliases with project references and ESM.


What was fixed

Over the course of a few iterations, the following simplifications were made:

  • Removed composite: true and project references — not needed unless using tsc --build
  • Kept global aliases in tsconfig.base.json (e.g., @tin-filters/*)
  • Explicitly included shared libraries in each tsconfig.json that needs them (include: ["src", "../../../libs/filters"])
  • Avoided using aliases at runtime in ts-node — replaced with relative imports (../../libs/filters/...) to avoid ESM issues
  • Cleaned .js artifacts from src/ to prevent ts-jest warnings
  • Restored commitlint hook using husky install and verified permissions

Key takeaway

If you’re not using tsc --build, don’t use composite: true.
If you want path aliases to „just work”, keep them purely TypeScript-side — not at runtime.
Simpler is safer.


What’s next

The filter works. The monorepo builds. The aliases resolve.
Next step? Back to building the actual system — now with one fewer architectural landmine in place.


Opublikowano

w

,

przez

Komentarze

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *