Source maps: debugging a minified production error
A customer reports that the invoice page crashes. You open your error tracker and find this:
TypeError: Cannot read properties of undefined (reading 'id')
at t.n (main.4f2a9c.js:1:88213)
at o (vendor.8b1e.js:1:412907)
There is no file you recognise, no function name you wrote, and one line number for a file that is a single 900 KB line. The same bug on your laptop would have taken two minutes to find.
Short version
- Build tools rewrite your code for production: names get shortened, whitespace is removed, files are merged.
- That is why production traces point at
main.4f2a9c.js:1:88213instead ofInvoiceRow.tsx:42:19. - A source map is a JSON file that maps every position in the built file back to the original file, line, and column.
- Generate maps with one build setting, then upload them to your error tracker so traces are translated automatically.
- Do not serve public maps for private code. Use hidden maps: build them, upload them, do not deploy them.
- Maps must match the exact build. A version mismatch produces confident but wrong line numbers.
Why the trace is gibberish
Before your code reaches a browser, a bundler such as webpack, Vite, esbuild, or Rollup runs a few transformations:
- Minification. Long names become short ones.
calculateInvoiceTotalbecomest. This is safe for the machine and terrible for humans. - Bundling. Hundreds of files are merged into a handful.
- Compilation. TypeScript, JSX, and modern syntax are rewritten into plain JavaScript.
- Whitespace removal. The result is often one very long line, which is why every frame says
:1:.
The browser reports honestly. main.4f2a9c.js:1:88213 really is where the error happened — 88,213 characters into the first line of the built file. The problem is that nobody can read that.
The column number is the only real clue in a minified trace, and it is exactly the piece a source map needs.
What a source map is
A source map is a JSON file, usually named after the file it describes, such as main.4f2a9c.js.map. It contains:
| Field | What it holds |
|---|---|
version | The source map spec version, currently 3. |
file | The name of the generated file, main.4f2a9c.js. |
sources | The list of original files, such as src/InvoiceRow.tsx. |
sourcesContent | The original source text, optional but very useful. |
names | The original identifiers, such as calculateInvoiceTotal. |
mappings | A compressed table linking built positions to original ones. |
The mappings field is a long string of base64 VLQ segments. You will never read it by hand, and you do not need to. Its job is to answer one question: "position 88213 on line 1 of the built file corresponds to which line and column of which original file?"
Feed a minified trace and a matching map into a tool, and you get back:
TypeError: Cannot read properties of undefined (reading 'id')
at calculateInvoiceTotal (src/InvoiceRow.tsx:42:19)
at renderRows (src/InvoiceTable.tsx:88:7)
Same error. Now it is actionable.
Generating source maps
Most build tools produce maps with one setting. Turn it on for production builds, not just development.
Vite — maps are on in dev by default. For production, in vite.config.js:
export default {
build: {
sourcemap: 'hidden',
},
};
webpack — in webpack.config.js:
module.exports = {
mode: 'production',
devtool: 'hidden-source-map',
};
esbuild — pass --sourcemap=external.
Next.js — set productionBrowserSourceMaps: true in next.config.js, or use the official error-tracker plugin, which handles hidden maps and upload for you.
The values matter. Here is what the common ones do:
| Setting | Map produced | Comment in the bundle | Deployed publicly |
|---|---|---|---|
source-map | Yes | Yes | Yes |
hidden-source-map | Yes | No | No, if you delete it |
inline-source-map | Embedded | Embedded | Yes, always |
eval-source-map | Embedded | Embedded | Development only |
false / none | No | No | Nothing to leak |
hidden is the setting most production apps want: the map exists on your build machine so you can upload it, but no //# sourceMappingURL= comment points browsers at it.
Should the map be public?
This is the trade-off that stops teams from using source maps at all.
Public maps mean anyone can open DevTools on your site and read your original source, including comments, internal file paths, and any secret accidentally left in the code. For open-source projects this costs nothing. For a private product it is a real disclosure.
Hidden maps give you readable traces in your error tracker with no public exposure. The cost is that DevTools on production will still show minified code, because the browser has nothing to fetch.
A middle path exists: keep maps hidden, but let developers load them locally. In Chrome DevTools, open the Sources panel, right-click the minified file, and choose Add source map..., then paste a URL or a local path. This works well for debugging a specific incident.
Whatever you choose, never serve a map for code that contains secrets. And remember the obvious rule: if a value must stay secret, it does not belong in frontend code at all, map or no map.
Uploading maps to an error tracker
Error trackers such as Sentry, Rollbar, Bugsnag, and Datadog all work the same way. You upload the maps at build time; they translate incoming traces automatically. The key is the release identifier, a string that ties a trace to the exact build it came from.
A typical flow:
- Pick a release name. The git commit SHA is the usual choice, such as
a1b2c3d4. Use the same string everywhere. - Tell the app its release. In your init code, set
release: 'a1b2c3d4', usually from an environment variable set by CI. - Build with hidden maps. Your
dist/folder now containsmain.4f2a9c.jsandmain.4f2a9c.js.map. - Upload the maps in CI, after the build and before the deploy. Every tracker ships a CLI for this, for example a command that uploads a folder of
.jsand.mapfiles under a named release. - Delete the maps from the deploy artifact so they never reach your web server. A single
rm dist/**/*.mapstep after upload does it. - Deploy.
- Verify. Trigger a test error on production and confirm the trace in the tracker shows real file names.
Step 7 is the one people skip and the one that catches every misconfiguration.
When maps do not work
Almost every failure comes from a mismatch. Work through these in order.
The release does not match. The app reports release: 1.4.0 but the maps were uploaded under a1b2c3d4. The tracker has no way to connect them. Print the release string in your build logs and compare it with what the tracker shows on the event.
The file names do not match. Maps were uploaded with paths like /build/main.js but the browser reports https://app.example.com/static/js/main.js. Trackers use a URL prefix setting, often ~/static/js/, to line these up.
The map is stale. You rebuilt without re-uploading. The line numbers resolve, but they point at the wrong code — which is worse than no map, because it looks correct. Always upload as part of the same CI job that builds.
The map was never generated. Check dist/ for .map files. If they are missing, your production build config has sourcemaps off.
The map has no sourcesContent. You get the right file name and line number but no code preview. Enable sourcesContent in your bundler, or accept the file-and-line output.
The error came from a browser extension or a third-party script. No map of yours will help. Frames pointing at chrome-extension:// or a vendor CDN are not your build.
Reading a translated trace
Once maps are working, treat the trace like a normal one. Two habits help:
- Trust the frame, verify the value. The trace tells you where the code was. It does not tell you what the data looked like. Pair the trace with the request that fed it.
- Check the code preview against the release. If your tracker shows the source line, make sure it looks like the code that shipped, not the code on your branch today. A confusing preview usually means the release tag is off by one deploy.
A useful sanity check when you first set this up: throw a deliberate error from a known file on staging, such as https://staging.example.com/debug/boom, and confirm the tracker names that file and line exactly.
Frequently asked questions
Do source maps slow down my site? No. With hidden maps the browser never downloads them. Even with public maps, they are only fetched when DevTools is open.
Can I unminify a stack trace without an error tracker?
Yes. Command-line tools and small libraries accept a minified file, a .map, and a line and column, and return the original position. It works, but it is slow for anything beyond a one-off.
Why does my trace show the right file but the wrong line? Almost always a stale map. The build changed after the map was uploaded. Rebuild and re-upload from the same CI job.
Are source maps a security risk? Public ones expose your original source, comments, and file structure. Hidden maps avoid that. Either way, never put secrets in frontend code.
Do I need maps for CSS too? Only if you debug compiled CSS from Sass or Tailwind. CSS maps help in DevTools but are not needed for JavaScript stack traces.




