Debugging TypeScript Code (Source Maps Revisited)
Introduction
Debugging TypeScript projects often feels like fighting a two-headed beast: you write beautiful, strongly typed code in .ts files, but the runtime runs compiled JavaScript. When an error occurs, the stack trace points to a generated bundle or a transpiled file and not to the original TypeScript source. Source maps were designed to close that gap, but they come with configuration pitfalls, performance trade-offs, and interaction quirks with bundlers, runtimes, and devtools.
In this guide we'll revisit source maps from an intermediate developer's point of view. You'll learn how source maps work under the hood, how to configure TypeScript, Webpack, Rollup, Vite, and Node so stack traces point at your original .ts files, and how to diagnose common breakages. We'll cover browser devtools mapping, Node runtime support (including the --enable-source-maps flag and source-map-support), inline vs external maps, hidden maps for production, mapping for minified bundles, and how to debug problems when eval/new Function/create dynamic code is involved. We'll include actionable examples, sample configs, and step-by-step troubleshooting checklists.
By the end you'll be able to: generate correct source maps for multiple build tools, convert unreadable stack traces into actionable line numbers, handle async stack traces (promises/async iterators), and adopt source map practices that balance DX and performance.
Throughout the tutorial, you'll find links to related TypeScript topics like error typing, debugging async code, and pitfalls around dynamic code constructs. These are useful follow-ups once you have source maps working and want to harden runtime error handling.
Background & Context
Source maps are JSON files that map generated JavaScript locations (file, line, column) back to original source locations. They enable browsers and runtimes to show original files and line numbers in stack traces and debugger UIs.
A minimal source map contains "version", "file", "sources", "mappings", and optionally "sourcesContent". The mappings are encoded with a compact but complex base64 VLQ format: this compresses mapping information so tooling can ship small maps while still reconstructing exact source ranges.
Tooling differences matter: TypeScript (tsc) can emit maps, but bundlers like Webpack and Rollup transform modules and must adjust mapping metadata. "devtool" options in Webpack, Rollup plugin settings, and flags for esbuild/Vite control the type and quality of maps (cheap versus full, inline versus external). Runtimes (browsers vs Node) use different mechanisms to consume source maps: browsers rely on the sourceMappingURL comment or HTTP header; Node (v12+) supports --enable-source-maps and tools like source-map-support add mapping to stack traces at runtime.
Understanding where the map is generated, how it's referenced, and how your runtime will consume it is the key to reliable debugging.
Key Takeaways
- Source maps map generated JS back to original TS/JS source and enable readable stack traces and debugging.
- Configure both the transpiler (tsc/Babel) and bundler (Webpack/Rollup/Vite) to generate compatible maps.
- Use inline maps for local development and external or hidden maps for production with privacy considerations.
- For Node, prefer --enable-source-maps or source-map-support to unminify stack traces.
- Async errors need special attention — promises, async iterators, and new Function/eval can break mappings.
- When maps look wrong, check sourceMappingURL, path normalization, embedded sourcesContent, and bundler output templates.
Prerequisites & Setup
Before you begin, make sure you have:
- Node.js (v14+ recommended) and npm/yarn
- TypeScript installed in your project (npm i -D typescript)
- A bundler (Webpack, Rollup, Vite) or knowledge of using tsc directly
- Familiarity with your editor's debugger (VS Code) and the Chrome DevTools
Starter commands you'll use in examples:
- npm install --save-dev typescript source-map-support
- tsc --build or your bundler's dev server (webpack-dev-server / vite)
If you're debugging server-side Node, ensure you can start Node with flags (e.g., node --enable-source-maps dist/index.js) or run via ts-node with source-map support.
Main Tutorial Sections
1) Anatomy of a Source Map (what each field means)
A source map is a JSON object. A tiny example:
{
"version": 3,
"file": "bundle.js",
"sources": ["src/index.ts"],
"sourcesContent": ["console.log('hi')"],
"mappings": "AAAA"
}Key fields:
- "file": the generated file name
- "sources": array of original source paths
- "sourcesContent": optional embedded original source text
- "mappings": base64 VLQ-encoded segments mapping generated positions to original positions
Embedding sourcesContent helps debug when the server cannot serve original source files. When debugging wrong line numbers, inspect this file—the problem often stems from wrong "sources" paths.
2) TypeScript compiler options for reliable maps
In tsconfig.json enable maps and (optionally) inline sources:
{
"compilerOptions": {
"sourceMap": true,
"inlineSources": false,
"inlineSourceMap": false,
"declaration": false
}
}Important options:
- "sourceMap": generate .js.map files
- "inlineSourceMap": embed the map directly into the .js file (useful for local debugging)
- "inlineSources": include original TS text in the map
If you're bundling afterwards, you may prefer ts-loader/babel-loader to output maps that the bundler will rebase. Avoid double-inlining maps unless you understand the build pipeline.
3) Webpack devtool choices and best practices
Webpack’s devtool controls map quality and build speed. Common choices:
- "eval-cheap-module-source-map": fastest, approximate mapping
- "cheap-module-source-map": good balance for dev
- "source-map": full maps but slower
- "hidden-source-map": generate maps but don't reference them in bundles (upload to error tracker instead)
Example webpack config snippet:
module.exports = {
devtool: 'cheap-module-source-map',
module: { rules: [{ test: /\.tsx?$/, use: 'ts-loader' }] }
}If your devtools show wrong paths, set output.devtoolModuleFilenameTemplate to ensure correct absolute/relative paths. For example: devtoolModuleFilenameTemplate: info => info.absoluteResourcePath.replace(/\\/g, '/').
4) Node: --enable-source-maps vs source-map-support
Node >=12 introduced experimental flag support; modern Node versions provide a native --enable-source-maps flag. Launch Node as:
node --enable-source-maps dist/index.js
For older versions or richer features (mapping unhandled exceptions at runtime), use the source-map-support package:
require('source-map-support').install();source-map-support ensures stack traces that reach your error handler are mapped back to original files. Remember to install it in production if you need readable server-side traces.
5) Inline vs External Source Maps: trade-offs
Inline source maps embed the mapping into the compiled .js via a base64 data URI. Pros: always available locally and for quick debugging; cons: increases file size and can leak source in production. External maps keep generated bundles lean; combine with hidden-source-map for private mapping that’s only uploaded to error trackers.
Typical workflow: use inline maps for local development, external maps for CI artifacts, and hidden/external maps uploaded to Sentry or similar for production diagnostics.
6) Bundling & minification issues (mapping minified code)
Minifiers (Terser, esbuild) transform code and must generate updated maps. If you minify after bundling, ensure the minifier consumes the previous map and produces a new one that points back to original sources:
- Webpack + Terser plugin handles it automatically if devtool is correct.
- esbuild or terser CLI require --sourcemap and the input map.
If minified maps point to the bundle instead of source files, check the minifier config to accept and merge input source maps. Also confirm your bundler isn't stripping sourceMappingURL comments.
7) Debugging async stacks: Promises, async/await, and iterators
Async code can produce confusing stack traces where the shown stack does not include the original async call chain. To get better traces:
- Enable source maps in the runtime.
- Avoid swallowing errors in detached promise chains.
- For complex async flows (generators/async iterators), ensure transpilation preserves async function boundaries.
If you work with typed async constructs, see the guide on Typing Promises That Reject with Specific Error Types in TypeScript for patterns to make errors easier to identify and type. For async iterators, the guide on Typing Asynchronous Generator Functions and Iterators in TypeScript helps you confirm runtime shapes during debugging.
8) Dynamic code: eval, new Function, and source maps
When runtime code is generated with eval() or new Function(), mapping back to original TypeScript is fragile or impossible depending on how maps are emitted. If you must use eval or new Function, you can embed a sourceURL comment to make DevTools treat the eval’d code as named code:
eval("function f(){throw new Error('boom')}
//# sourceURL=dynamic-module.ts");But the mapping won't reconstruct original TypeScript unless you supply an accompanying source map. Also, dynamic code is a security surface—see our cautionary articles on Typing Functions That Use eval() — A Cautionary Tale and Typing Functions That Use new Function() (A Cautionary Tale) for typing and risk guidance before relying on such approaches.
9) VS Code debugging: mapping breakpoints to TS files
VS Code's debugger uses the same source map mechanisms. Example launch.json for Node:
{
"type": "pwa-node",
"request": "launch",
"program": "${workspaceFolder}/dist/index.js",
"runtimeArgs": ["--enable-source-maps"],
"outFiles": ["${workspaceFolder}/dist/**/*.js"]
}Set "outFiles" so VS Code knows where to find generated JS and maps. If breakpoints are not hitting, ensure that the path formats in source maps match how VS Code resolves files (absolute vs relative, normalize backslashes).
10) Error objects, typing, and richer diagnostics
Better stack traces are only useful if errors are typed and carry contextual data. Adopt consistent error types and attach useful metadata so traces contain actionable info. See the guide on Typing Error Objects in TypeScript: Custom and Built-in Errors for patterns to structure errors so that when source maps reveal the original file and line, you also get typed error properties.
Also consider typing the JSON responses your code consumes — validated payloads reduce time spent debugging malformed data. Our guide on Typing JSON Payloads from External APIs (Best Practices) explains reliable validation patterns that help narrow down source map investigations to concrete bad input.
Advanced Techniques
- Use hidden-source-map in production: generate maps, but don’t include sourceMappingURL. Upload maps to error reporting tools (Sentry, Rollbar) and serve them only to trusted systems.
- Configure bundler filename templates: set devtoolModuleFilenameTemplate and output.sourceMapFilename to normalize paths (convert Windows backslashes to forward slashes, strip absolute prefixes).
- Embed sourcesContent when you cannot serve original sources, enabling devtools to show original files even if repository is not available.
- For Node servers, use source-map-support with a cache to reduce mapping overhead in high-QPS services and precompute mappings for long stack traces.
- Use sourcemap-explorer and source-map-visualization tools to inspect mappings and find unmapped ranges in huge bundles.
If you are debugging complex async flows, combine runtime source maps with better error typing; see Typing Promises That Resolve with Different Types for patterns that keep error shapes predictable.
Best Practices & Common Pitfalls
Dos:
- Do enable sourceMap in tsconfig and ensure the bundler consumes those maps correctly.
- Do use inlineSourceMap for rapid local iteration and external maps for CI/publishing.
- Do normalize paths in bundler templates to avoid mismatched source paths in devtools and IDEs.
- Do include sourcesContent when the original files are not served.
Don'ts:
- Don’t ship inline source maps to production if you want to hide your source code — use hidden-source-map and upload to your error tracker.
- Don’t assume stack traces are accurate; verify by checking the source map file format and contents.
- Don’t ignore dynamic code: eval/new Function often lose mapping. Consult Typing Functions That Use new Function() (A Cautionary Tale) and Typing Functions That Use eval() — A Cautionary Tale for safer alternatives.
Troubleshooting checklist:
- Confirm generated .map files exist and that the bundle includes a correct "//# sourceMappingURL" comment.
- Open the .map file and inspect "sources" and "sourcesContent".
- Ensure your devtools/IDE picks up the same paths (normalize separators).
- Rebuild with a full map mode (e.g., Webpack's "source-map") to see if the issue is bundler optimization.
- If Node, try node --enable-source-maps or require('source-map-support').install().
Real-World Applications
- Backend services: attach source maps to error aggregates so production stack traces reference .ts files and preserve developer time when investigating incidents.
- Front-end single-page apps: configure hidden-source-map for releases and upload maps to Sentry to map minified user errors back to your source.
- Library authors: ship external source maps for consumers while ensuring privacy by excluding sourcesContent or using source map consumer-only uploads.
When debugging user-reported issues, good source maps let you reproduce the problem locally by setting breakpoints on the original TypeScript lines and replaying the failing steps. For complex async or iterator-based flows, refer to the typing guides for async iterators and Promise error patterns; helpful links: Typing Asynchronous Generator Functions and Iterators in TypeScript and Typing Promises That Reject with Specific Error Types in TypeScript.
Conclusion & Next Steps
Source maps are an essential tool in the TypeScript developer toolkit, but they require careful configuration across the compiler, bundler, and runtime. Start by enabling maps in tsconfig, choose sensible bundler devtool options, and validate maps in the browser/Node. For production, adopt hidden maps and upload them to your error tracking system. Next, strengthen runtime error typing and async diagnostics so mapped stack traces give you both the location and the context you need. Consider following up with the linked TypeScript articles to harden error typing and dynamic-code usage.
Enhanced FAQ
Q1: My stack trace shows the bundled file and wrong line numbers. What should I check first?
A1: First ensure a .map file exists for that bundle and that the bundle includes a correct sourceMappingURL comment or that an HTTP header points to the map. Open the .map file and check the "sources" array: if paths are absolute or Windows-style with backslashes, you may need to normalize them in bundler templates (e.g., devtoolModuleFilenameTemplate). Also ensure the minifier consumed earlier maps when producing the final map.
Q2: Why are breakpoints in VS Code not hitting my TypeScript files?
A2: VS Code needs to know where the compiled JS and maps live. In launch.json, set "outFiles" to match the dist pattern so VS Code can find them. Also verify the maps contain source paths matching how VS Code resolves files. If needed, rebuild with full maps ("source-map") and inspect the map's "sources" values.
Q3: Is it safe to use inlineSourceMap and inlineSources in production?
A3: Generally no. Inline maps embed the entire map (and optionally sources) into the JS file, leaking your original source. For production, use external maps and keep them hidden (hidden-source-map) or upload them only to your error-tracking provider.
Q4: Stack traces from promises lack the original call site—how to fix?
A4: Ensure runtime source maps are enabled (node --enable-source-maps or source-map-support). Also, avoid swallowing promise rejections. Instrument your code to rethrow or log context. Stronger typing for errors helps—see Typing Promises That Reject with Specific Error Types in TypeScript to keep error shapes consistent and easier to trace.
Q5: DevTools shows the original source but the code content is empty or outdated—what happened?
A5: If the map references sources but doesn't include sourcesContent, DevTools will try to fetch the original files from the server. If those files are not served (or paths are wrong), you'll see missing content. Solutions: embed sourcesContent in the map or ensure your dev server serves the original source files at the paths listed in "sources".
Q6: My project uses dynamic code generation via eval/new Function. Can source maps help?
A6: Dynamic code complicates mapping. You can add a sourceURL comment to name the eval code, which helps DevTools show it by name. However, reconstructing original TypeScript requires providing a proper source map for that generated code. Because of security and maintainability reasons, prefer alternatives to eval/new Function and consult the cautionary pieces on Typing Functions That Use eval() — A Cautionary Tale and Typing Functions That Use new Function() (A Cautionary Tale).
Q7: What are the performance implications of source maps in CI/builds and production?
A7: Generating full source maps increases build time and may increase artifact sizes. Minifiers that merge source maps add additional CPU cost. In CI, consider generating maps only for release builds and use faster map modes (cheap maps) for local dev. In production, avoid shipping large inline maps to clients and use hidden maps that are uploaded to your error-tracking service so they don’t affect runtime performance or download size.
Q8: How do I inspect a source map to find where a generated line maps in the original source?
A8: Use tools like source-map-explorer, source-map-cli, or load the .map file in a small script that uses the source-map library to query mappings. For example, with the source-map npm package you can load a map and call originalPositionFor to translate generated line/column to source file/line/column. This is useful for automated tests or when debugging huge bundles.
Q9: Should I rely on bundler defaults for devtool or configure explicitly?
A9: Explicit configuration is safer. Defaults change across bundler versions and may not fit your needs (speed vs accuracy). Choose a devtool that balances iteration speed for development and accuracy for debugging. Use full "source-map" mode selectively when you need precise mapping to solve tricky bugs.
Q10: Any recommended follow-up reading?
A10: Once your source maps are reliable, strengthen runtime error handling and typing. Read up on Typing Error Objects in TypeScript: Custom and Built-in Errors to design better error types, and explore Typing Functions with Context (the this Type) in TypeScript if you frequently debug "this" binding issues. For better input validation and to reduce debugging time from bad payloads, review Typing JSON Payloads from External APIs (Best Practices).
