Source Maps

Source Maps Explained: Debug Minified JS in Prod

Source maps map minified code back to your original files. Learn what they are, how they work, and how to use them to debug production errors.

Your production JavaScript is minified to save bandwidth: variable names become single letters, whitespace vanishes, and 50 lines collapse into one. This is great for performance. It's terrible for debugging. When a user's error lands in your dashboard as t.u is not a function at index-4f9a.js:1:52210, you're looking at obfuscated code from line one. Source maps explained — and how to use them — turns that garbage back into the exact line of your real code, so you can fix errors instead of guessing.

Source maps bridge the gap between what users run (minified) and what you wrote (readable). They're a critical piece of production debugging, especially in JavaScript where stack traces are your primary tool for understanding what broke.

What a source map is

A source map is a file that translates locations in minified code back to their original positions in your source files. It's a mapping table — for every character position in the bundled output, it records the matching line and column in the original .js, .ts, or .jsx file.

When you minify src/app.ts down to dist/app-4f9a.js, the build tool creates dist/app-4f9a.js.map. That .map file contains coordinates: "this character at position 52210 in the bundle came from line 47, column 8 in src/app.ts." Your error tracker uses the map to decode the stack trace back to your original source.

Minified code:           const t={u:()=>{throw new Error('x')}};t.u();
Source map says:         "This came from src/app.ts, line 5"
Original code:           const handler = { execute: () => { throw new Error('x') } }; handler.execute();

Without the map, you see t.u is not a function. With it, you see handler.execute is not a function — and you know exactly where to look.

How source maps work

Source maps use a compressed, base64-encoded format that fits alongside your deployed bundle without doubling your file size. The key insight is that they don't include your source code — they only map coordinates. This means:

  • The .map file is tiny — usually 10–30% of the bundle size, far smaller than shipping unminified code.
  • Source code stays private — the map alone is useless without your original files. An attacker can't reverse-engineer your app logic from a .map file.
  • Errors are decoded server-side — most error trackers (including LightTrace) decode stack traces as events arrive, not in the browser, so your users never download the maps.

The underlying mechanism is clever. When minification happens, every character position in the output is tracked back to its origin in the source. A source map is just a compressed lookup table of those positions. Your error tracker loads the map for the matching release and version, looks up the position of an error in the minified stack trace, and translates it to the original line and column. All of this happens invisibly on the server.

The format itself is standardized and simple. Each encoded line maps a position in the minified output to a position in the source, with the filename and the source line/column:

{
  "version": 3,
  "sources": ["src/app.ts", "src/utils.ts"],
  "sourcesContent": ["// original source code here", "// ..."],
  "mappings": "AAAA,IAAI,CAAC,GAAG,EAAE,CAAC;AACX,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC"
}

The mappings field uses variable-length quantities (VLQ) to compress thousands of coordinates into a short string. It's dense but mechanical — a decoder steps through the minified code and looks up each position to find the original location.

Building source maps in your bundler

Modern bundlers — Vite, Webpack, Esbuild, Parcel — all generate source maps during the build. Turning them on is usually one line in your config.

Vite:

export default defineConfig({
  build: { sourcemap: true }
});

Webpack:

module.exports = {
  mode: 'production',
  devtool: 'source-map', // Production-grade, full maps
  // Or: devtool: 'hidden-source-map' if you'll upload separately
};

Esbuild:

esbuild.buildSync({
  entryPoints: ['src/index.ts'],
  bundle: true,
  minify: true,
  sourcemap: true, // or 'inline' or 'external'
});

In production, use source-map (Webpack) or true (Vite/Esbuild) to generate separate .map files. Never use inline-source-map in production — it embeds maps inside bundles, bloating them. You upload maps to your error tracker separately.

Uploading source maps to your error tracker

Generating maps locally is half the job. The other half is getting them to your error tracker so it can decode incoming errors. The standard approach is to upload maps in CI as part of your deploy pipeline.

LightTrace has a source map upload API. After your build, you post each .map file with its matching release tag:

# After npm run build, upload maps
curl -X POST https://your-lighttrace-host/api/releases/web%401.0.0/files/ \
  -H "Authorization: Bearer <token>" \
  -F "file=@dist/app-4f9a.js.map" \
  -F "name=app-4f9a.js.map"

For a complete walkthrough with your specific framework, see how to upload source maps. For Next.js and Vite, we have step-by-step setup guides that automate this: Next.js source maps and Vite source maps.

Tag every upload with a release. When you deploy version web@1.4.2, upload maps for exactly that release. Then when an error from that version lands in your error tracker, it finds the matching maps and decodes the trace. No release tag = no decoding.

Why source maps matter for error tracking

Here's where source maps pay for themselves. Without them, every production error is a mystery — you're trying to debug code you've never seen, in a format you didn't write. With them, you get the same experience you have locally: readable function names, real file paths, the exact line. The difference between a quick fix and a 2-hour guessing game often comes down to whether your error tracker can decode your stack traces.

Compare:

Without mapsWith maps
TypeError: t.u is not a function at index-4f9a.js:1:52210TypeError: handler.execute is not a function at src/checkout.ts:47:8
You have to reverse-engineer the minified codeYou can read the exact source and see the bug immediately
Minutes or hours to narrow down the issueSeconds from error to fix
No way to reproduce — you're debugging blindFull context and breadcrumbs show the exact user path

An error tracker like LightTrace uses the maps to do this automatically. Every stack frame is decoded the moment an error arrives, so your dashboard shows readable, linkable traces. You can click directly from a frame to the exact line on GitHub (if you've enabled source links), or search your codebase by the real function name instead of a minified letter. This transforms error triage from a painful process into something you can actually do in minutes. For more on how to read and debug from stack traces, see how to read a stack trace.

Source maps across frameworks and bundlers

Source map generation is standard, but the details vary by framework. You'll likely want a dedicated guide:

  • Webpack — multiple devtool strategies and build optimizations.
  • Vite — fastest builds, simplest config.
  • Next.js — server and client maps, with setup for TypeScript.

All of them follow the same pattern: enable generation in dev/prod config, and upload in CI. The specific commands differ, but the idea is identical. Many teams use their error tracker's CLI or GitHub Actions integration to automate uploads so developers never think about it.

Common mistakes with source maps

Forgetting to upload maps to your error tracker. Source maps have to be on your error tracker's server to decode anything. If you're seeing minified traces, the maps probably didn't make it. Check that your release tag in the upload matches the release tag in your events. This is the most common mistake — maps are generated locally, but developers forget to set up the CI step that pushes them to the error tracker. One missed deploy and you're back to cryptic minified errors.

Using inline maps in production. Inline source maps work great locally for debugging, but they balloon your bundle size in production. A single inline source map can add 500KB or more to your bundle, tanking performance for every user. Always use separate files in production and upload them separately.

Not tagging your builds with a release. Without a release tag, your error tracker can't correlate events to maps. Tag every deploy — use semantic versioning like web@1.4.2 or an environment + timestamp like prod-2026-07-03-14-22. Without it, your error tracker can't find the matching maps even if they're uploaded.

Serving maps to browsers unnecessarily. Your CDN shouldn't serve .map files by default. Keep them on your error tracker's server only. Most bundlers don't publish maps to your CDN by default, but if you're custom-publishing assets, make sure maps stay private. Check your CDN's cache rules to confirm .map files aren't being served to the public.

Exposed source maps are a security risk — they leak your code structure, variable names, and logic. Always upload maps to your error tracker (which is private to your team) and keep them off your public CDN. If maps are exposed, anyone can reverse-engineer your code faster than reading minified JavaScript.

Performance considerations

Source map file sizes are usually small (10–30% of minified bundle size), but they add up for large applications. The maps themselves don't slow down the user's browser — they live on your error tracker's server. The overhead is:

  • Build time — generating maps adds a few seconds to your build.
  • Upload time — posting maps in CI is typically < 10 seconds.
  • Error decoding time — LightTrace decodes traces server-side, so errors appear with readable names instantly, no client-side penalty.

For the vast majority of teams, the payoff (debuggable production errors) far outweighs the overhead (a few extra seconds per deploy).

Next steps

You're ready to ship with source maps. Start with:

  1. Enable source maps in your build config — a one-line change in Webpack, Vite, or Esbuild.
  2. Tag every release. Set a version identifier like web@1.4.2 for every deploy.
  3. Upload maps in CI. Add a curl or CLI command to post maps after your build.

For more detail on automation and your specific framework, see upload source maps. If you already have errors in production and need help decoding them, see debug minified JavaScript.

Start tracking errors in minutes

Point the Sentry SDK at LightTrace, enable source maps in your build, and ship readable production stack traces — start free, no credit card required.

Source maps are table stakes for production debugging. Once you've got them wired up, every stack trace tells you the real story instead of a cryptic minified mystery. Your next production bug becomes a 5-minute fix instead of a 2-hour guessing game.

Fix your next production error faster

Point any Sentry SDK at LightTrace — free up to 5,000 events/month.