Source Maps Not Working in Production? Fixing Minified Stack Traces
That stack trace pointing to line 1, column 38291 should have been pricing.ts:47. Here's how to fix source maps that refuse to work in production.
Picture an alert at 4:12pm: a TypeError in checkout. Opening the error tracking dashboard, you'd expect the exact file and line number so you could fix it before dinner.
Instead:
TypeError: Cannot read properties of undefined (reading 'amount')
at main.f7a3b1.js:1:38291
at main.f7a3b1.js:1:38847
at main.f7a3b1.js:1:41203
Line 1. Column 38,291.
Source maps set up months ago can work fine in staging. The upload step sits in the CI pipeline and looks right. But production errors still show minified garbage.
A common culprit: the build outputs source maps to dist/static/js/, but the upload command looks in dist/. The maps exist. They just never get uploaded. Classic. And nobody notices because staging uses a different build config that puts maps in the right place.
This tutorial covers the actual failure modes that come up when source maps refuse to work in production. Not the theory. Just the practical fixes for when it's broken and you need it working now.
Prerequisites
- Error tracking tool with source map support (JustAnalytics, Sentry, Datadog, Rollbar)
- A JavaScript/TypeScript build pipeline that generates source maps
- Access to your CI/CD configuration
- 15-30 minutes depending on how broken things are
What We're Fixing
By the end of this, your production errors will show:
TypeError: Cannot read properties of undefined (reading 'amount')
at calculateTotal (src/utils/pricing.ts:47:12)
at CheckoutForm (src/components/CheckoutForm.tsx:142:8)
at renderWithHooks (react-dom.development.js:...)
Instead of column 38,291, you get pricing.ts:47:12. That's the difference between a 5-minute fix and a 2-hour debugging session.
Step 1: Verify your build actually generates source maps
This sounds obvious. It's also a surprisingly common culprit.
Don't skip this step just because you're sure your config is fine.
Check your bundler config. For Next.js setup in general, see our Next.js 15 analytics tutorial. For Webpack:
// webpack.config.js
module.exports = {
mode: 'production',
devtool: 'source-map', // NOT 'eval' or 'cheap-source-map'
// ...
};
The devtool value matters more than you'd think. Webpack's devtool options are honestly confusing — there's like fifteen of them and half do basically the same thing. Here's what actually matters:
| devtool value | Produces .map files? | Quality |
|---|---|---|
source-map | Yes | Full — line and column accuracy |
hidden-source-map | Yes | Full — same as above, no sourceMappingURL comment |
cheap-source-map | Yes | Line-only — no column info |
eval-source-map | No (inline) | Fast but not uploadable |
eval | No | Nothing useful |
false / omitted | No | Nothing |
For production error tracking, you want source-map or hidden-source-map. I'd go with "hidden" — it skips the //# sourceMappingURL= comment so users can't find and download your maps from the browser. Not that your code is that interesting. But still.
For Vite:
// vite.config.js
export default {
build: {
sourcemap: true, // or 'hidden'
},
};
For esbuild:
// esbuild.config.js
require('esbuild').build({
sourcemap: true, // or 'external' or 'linked'
// ...
});
After configuring, run your production build and check the output directory. You should see .map files alongside your .js files:
ls -la dist/
# main.f7a3b1.js
# main.f7a3b1.js.map <- This needs to exist
# vendor.a3f8b2.js
# vendor.a3f8b2.js.map
If the .map files aren't there, your build config is wrong. Fix that first. No point debugging uploads for files that don't exist. Don't spend 45 minutes debugging an upload pipeline for maps that aren't being generated.
Step 2: Find where your build actually outputs maps
The maps can exist and still be in the wrong place.
Modern bundlers scatter output across subdirectories. Next.js puts chunks in .next/static/chunks/. Vite uses dist/assets/. Webpack depends on your output.path config.
Run this after a production build:
find dist -name "*.map" -type f
# Or for Next.js:
find .next -name "*.map" -type f
Note the exact paths. You'll need them for the upload command.
Common gotcha: code splitting. If your app uses dynamic imports, you'll have multiple chunk files with their own source maps:
dist/static/js/main.f7a3b1.js.map
dist/static/js/pages-checkout.a8f3b2.js.map
dist/static/js/pages-product.c4d5e6.js.map
dist/static/js/vendor.7e8f9a.js.map
Your upload command needs to grab all of them. A command that only uploads dist/*.map will miss the subdirectories. Teams can lose hours to this: the main bundle's errors show readable traces while the checkout errors stay minified, because the checkout chunk lives in a subfolder you're not uploading.
Frustrating? Absolutely. But at least it's fixable once you know.
Step 3: Upload source maps with the correct release tag
This is the failure mode that creates the most confusion. Your maps upload successfully. Your errors have source maps available. But they don't match.
The release tag on the upload must exactly match the release tag on the error. If your build tags errors with v2.4.1 but your upload command tags maps with 2.4.1 (no "v"), they won't correlate.
In JustAnalytics:
# In your CI pipeline, after build completes
# Files are positional arguments (globs work); --release and --site-id are required
npx @justanalyticsapp/cli sourcemaps upload \
--release="${APP_VERSION}" \
--site-id="${JA_SITE_ID}" \
"dist/**/*.map"
The CLI reads your API key from the JUSTANALYTICS_API_KEY environment variable (or an --api-key flag). Create one in your JustAnalytics dashboard.
The APP_VERSION here must match exactly the release string your app attaches to its errors. Find where your setup sets that value and make both sides read the same variable.
If your build script sets APP_VERSION to the git short hash:
export APP_VERSION=$(git rev-parse --short HEAD)
Then your upload must use the same:
npx @justanalyticsapp/cli sourcemaps upload \
--release="$(git rev-parse --short HEAD)" \
--site-id="${JA_SITE_ID}" \
"dist/**/*.map"
I recommend explicitly printing the release tag in your CI logs:
# GitHub Actions
- name: Upload source maps
run: |
echo "Uploading source maps for release: ${{ github.sha }}"
npx @justanalyticsapp/cli sourcemaps upload \
--release="${{ github.sha }}" \
--site-id="${{ secrets.JA_SITE_ID }}" \
"dist/**/*.map"
When debugging, you can check: what release does the error report? What release did the upload use? If they differ, that's your problem.
Step 4: Handle chunk hashes and cache busting
Modern bundlers add content hashes to filenames for cache busting: main.f7a3b1.js. This creates a timing problem.
If you upload source maps before the build finishes — or from a different build than the one that deployed — the hashes won't match. The uploaded map for main.a1b2c3.js won't help errors from main.f7a3b1.js.
The fix: upload source maps from the same build artifacts you deploy. In GitHub Actions:
jobs:
build-and-deploy:
steps:
- name: Build
run: npm run build
- name: Upload source maps
run: |
npx @justanalyticsapp/cli sourcemaps upload \
--release="${{ github.sha }}" \
--site-id="${{ secrets.JA_SITE_ID }}" \
"dist/**/*.map"
- name: Deploy
run: npm run deploy
The order matters. Build → Upload → Deploy.
Not negotiable.
If you deploy before uploading, there's a window where errors arrive before maps exist. If you upload from a different job that rebuilds, the hashes might differ.
For Vercel deployments, you need to upload during the build phase since you don't control the deploy step:
// package.json
{
"scripts": {
"build": "next build && npm run upload-sourcemaps",
"upload-sourcemaps": "npx @justanalyticsapp/cli sourcemaps upload --release=$VERCEL_GIT_COMMIT_SHA --site-id=$JA_SITE_ID \".next/**/*.map\""
}
}
Step 5: Verify uploads actually worked
Don't assume. Verify.
I'm serious — this step catches uploads that looked successful and weren't.
The CLI prints a line per file and a summary. Read it:
Uploading source maps for release "a3f7e21"...
Uploading [1/3] main.f7a3b1.js.map ... OK (847 KB)
Uploading [2/3] vendor.a3f8b2.js.map ... OK (1.2 MB)
Uploading [3/3] pages-checkout.c4d5e6.js.map ... OK (124 KB)
Successfully uploaded 3 source maps for release "a3f7e21" (0 failed)
Compare the count against find dist -name "*.map" | wc -l. They should match. There's no sourcemaps list command in the CLI, which I find an annoying gap: the only proof of a good upload is the upload output itself, plus a real test error (see below).
If the count is zero or a file is missing:
- The glob didn't match. Quote it, so your shell doesn't expand it first, and check the path from Step 2
- The release tag differs from the one your errors report
- Your API key is missing or invalid
- Some chunks aren't generating maps (check per-entry sourcemap config)
Common Errors and Fixes
"401 Unauthorized" during upload.
Your API key is missing or invalid. Check that JUSTANALYTICS_API_KEY is set in your CI secrets. Regenerate the key if needed.
No source map found for main.f7a3b1.js.
The minified filename doesn't match any uploaded map. Check that the map filename matches exactly. Some bundlers output main.f7a3b1.js.map while the error references main.f7a3b1.js — that's correct. But if the hash differs, you've got a build mismatch.
Stack traces resolve to the wrong lines. Your source maps are out of sync with your deployed code. This usually means the map was uploaded for a different build. Redeploy with fresh maps, or check that your release tags are unique per build.
DevTools doesn't pick up a source map.
If you're using hidden-source-map, this is expected — the comment is intentionally omitted. Your error tracking service doesn't need the comment; it matches files by name. DevTools won't show the source tab, but production error tracking will work.
Only some errors have resolved stack traces.
You're not uploading all chunk maps. Dynamic imports create separate chunks that need their own maps. Use recursive find: find dist -name "*.map" and make sure your glob (dist/**/*.map) reaches subdirectories. A CI step that compares the CLI's uploaded count with the find count catches this.
Testing the Full Flow
Before you need it at 3am, verify everything works:
- Deploy to a staging environment with your production build config
- Trigger a test error — add a temporary
throw new Error('Source map test')somewhere - Check your error tracking dashboard
- Does the stack trace show your original filename and line number?
If it doesn't, work backward:
- Is the release tag correct on both the error and the uploaded maps?
- Did the upload succeed? (Check the CLI summary in CI logs)
- Do the maps exist in the build output? (Check locally)
- Is
devtoolset correctly for production?
This test takes five minutes. Maybe ten if your staging deploy is slow. But you do not want to be figuring out your source map config at 3am when production is on fire and your CEO is awake and asking questions.
Next Steps
Once source maps are working, you'll actually be able to debug production errors. Honestly? I think source map configuration is the most under-documented part of modern frontend tooling. Every bundler does it differently, every CI system has its own quirks, and the docs all assume you already know what you're doing. Some related workflows:
-
Release health tracking: Tie error rates to specific deploys so you catch regressions immediately.
-
Funnel correlation: Find which errors are killing conversions, not just which errors exist. Our error-funnel correlation guide covers linking errors to checkout drop-off.
-
Alert tuning: Now that you can read stack traces, reduce noise from duplicate errors. Our alert deduplication guide shows how to group errors by root cause.
The difference between main.f7a3b1.js:1:38291 and pricing.ts:47:12 is the difference between guessing and knowing. Get your source maps uploaded. Future you — the one squinting at error logs at 4am — will thank present you.
Frequently Asked Questions
Why are my production stack traces showing line 1 and a huge column number?
Your source maps aren't being applied to incoming errors. Three common causes: source maps weren't uploaded to your error tracking service, the release tag on the upload doesn't match the release tag on the error, or your build is generating source maps but not outputting them to the expected directory. Check your CI logs for upload confirmation and verify the release string matches exactly.
How do I know if my source maps uploaded successfully?
Most error tracking CLIs output a confirmation with the number of files uploaded. The JustAnalytics CLI prints one line per file as it uploads, then a summary such as Successfully uploaded 3 source maps for release "v1.2.3" (0 failed). Check that the count matches the number of .map files in your build output and that the failed count is zero. The CLI has no command for listing uploaded maps, so the final check is to trigger a test error and read its stack trace.
Should I include sourcesContent in my source maps or keep them lean?
Include sourcesContent for production error tracking. Without it, your error tracking service can only show file and line number — not the actual code snippet. The file size increase is worth it. Uploaded maps go to your error tracker over an authenticated API, so as long as you don't also deploy the .map files to your public server, embedding the source doesn't expose it. A 2MB source map with embedded content beats a 400KB map that shows "PaymentForm.tsx:142" with no context.
Why do my source maps work locally but not in production?
Local dev builds and production builds often use different source map configurations. Check that your production build command generates the same source map type (usually "source-map" or "hidden-source-map") as your local build. Also verify your CI is uploading from the correct output directory — production builds often output to different folders than dev builds.
Try JustAnalytics
All-in-one observability in one under-5KB script: cookieless analytics + error tracking + APM + session replay + uptime + structured logs. Replaces GA4 + Sentry + Datadog + Pingdom + LogRocket. Free tier (100K events/mo), Pro $49/month ($39 annual).
Author at JustAnalytics.