Why your stack traces show minified function names (and how to fix it)

Source map upload is one of three preconditions. Here are all three, and how to verify each in production.

·
source-mapstutorial

It's 3:00 AM, and a "Critical Error" alert just woke you up. You open your error tracker, ready to find the offending line, but instead of processPayment() or validateUser() you're staring at a sea of single-letter gibberish: at a (main.js:1:4302). This is mystery-meat debugging. Your error tracker can see the crime scene perfectly—it just doesn't have the map to translate those coordinates back into your actual source code.

The standard advice is "upload your source maps," and it's incomplete. Source-map upload is just one of three things that all have to go right. Unminification is a three-way handshake: generation at build time, availability after upload, and association at runtime. Break any one link and the trace stays minified—even if the other two are flawless. This post walks all three, in order, with a checklist to verify each in production.

The mystery-meat problem: why we minify

We minify because bytes cost time. Tools like Terser, esbuild, and SWC strip whitespace, remove comments, and rename calculateMonthlyRecurringRevenue down to e. The result is a bundle that downloads and parses faster—and a stack trace that's unreadable. main.js:1:4302 isn't line 4,302 of your code; it's character 4,302 of a file that crammed dozens of source files onto a single line.

How error trackers unminify on the fly

A source map is a JSON sidecar that records, for every position in the minified file, where it came from in the original source—including the original variable and function names. When your tracker has the right map, it reverses the transformation and shows you processPayment (src/checkout/pay.ts:42:9) instead of a (main.js:1:4302). For the gory internals of how that decoding works, see how to deminify a JavaScript stack trace.

The performance vs. debuggability trade-off

You don't have to choose. You ship the minified bundle to users for speed, and you give your error tracker the map for debuggability. The map never has to touch the browser. The whole game is making sure the tracker can find and apply the right one.

The three-way handshake of source maps

Hold this model for the rest of the post, because it's the diagnostic framework:

  • Generation (the build step): your bundler produces a high-quality .map file.
  • Availability (the upload step): that map reaches your error tracker, not your public CDN.
  • Association (the runtime match): the tracker matches the right map to the right error from the right deploy.

When traces are minified, exactly one of these three failed. Knowing which one saves you hours of flailing.

Step 1: generating the map

If the bundler never wrote a usable map, nothing downstream can work. The most common generation failure is choosing the wrong setting for the production environment.

Choosing the right devtool

In Webpack, the devtool option controls map quality. For production you want a real, separate, full-fidelity map. source-map produces one. hidden-source-map produces the same map but omits the sourceMappingURL comment from the bundle—ideal when you upload to a tracker and don't want browsers discovering the map. Avoid every eval-* variant in production: eval-source-map inlines maps into eval() wrappers for fast dev rebuilds and will not symbolicate correctly on a server.

// next.config.js — emit hidden source maps for the error tracker
module.exports = {
  productionBrowserSourceMaps: true,
  webpack(config, { dev }) {
    if (!dev) {
      // separate .map files, no sourceMappingURL leaked to the browser
      config.devtool = "hidden-source-map";
    }
    return config;
  },
};

One more generation gotcha: aggressive minifier settings can drop function names entirely, so even a perfect map resolves the line but shows <anonymous>. Setting keep_fnames: true in Terser preserves them at a bundle-size cost of typically only 1–3%.

Step 2: the upload

The map exists—now it has to reach your tracker, and it must not sit on your public server.

Why you never host maps publicly

Maps often embed your entire original source via sourcesContent. If main.js.map is reachable at example.com/main.js.map, anyone can download your unminified source code. hidden-source-map plus an upload step keeps the map out of the browser entirely. For the deeper treatment, see the source-maps documentation.

Automating the upload, and matching the release

Upload from CI, and tie the artifacts to a release identifier that the runtime SDK will report with the exact same string. This is where most teams quietly break Step 3 from inside Step 2.

# GitHub Actions — build, then upload BEFORE deploying
- name: Build
  run: npm run build

- name: Upload source maps
  env:
    RELEASE: "my-app@${{ github.sha }}"
  run: |
    sentry-cli sourcemaps inject ./.next
    sentry-cli sourcemaps upload --release "$RELEASE" ./.next

- name: Deploy
  run: npm run deploy

The release string used here must be byte-for-byte identical to the one your Sentry.init reports at runtime. Compute it once and reuse it; deriving it twice is the single most common reason maps that uploaded fine never match.

Step 3: the runtime match

This is where most setups fail, because association depends on three things lining up that are produced by three different processes at three different times.

The sourceMappingURL comment

When you upload to a tracker, you generally do not need the sourceMappingURL comment in your bundle—the tracker matches on release and file name, not on a URL it fetches. That's why hidden-source-map works: it strips the comment but the upload still carries everything the tracker needs. If your tracker fetches maps over the network instead, the comment matters and the URL must be externally reachable.

Absolute vs. relative paths

The path the SDK reports for a frame (the abs_path) must reconcile with the file name the artifact is stored under. Bundlers love to inject prefixes—webpack://, ~/, absolute build-container paths that don't exist on your CDN. If the runtime sees https://app.example.com/_next/static/chunks/main.js but the artifact is filed as ~/main.js, the match fails. Normalize one side to the other.

The release-tag mismatch

The headline killer. Three values must match exactly: the version ID (release string), the file path, and the project the artifacts were uploaded to. A trailing newline in an environment variable, a short SHA at runtime versus a full SHA at upload, or a stray case difference—any of these silently defeats the match. For a deep dive into the five flavors of this failure, see why your SDK isn't capturing source-mapped traces.

How to verify the fix in production

Don't wait for a real user to crash to find out whether you got it right. Test it deliberately.

Trigger a synthetic error

After deploy, fire a known error from the production bundle—a button wired to throw new Error("sourcemap-test") works fine—and watch it land in your dashboard. If it symbolicates, the handshake is complete. If it shows <anonymous> or minified names, you've isolated the failure to a single deploy you can inspect immediately.

Read the processing log and artifacts tab

Both GlitchReplay and Sentry surface a processing log that tells you exactly why symbolication failed—missing artifact, release mismatch, unreachable map. Check the Artifacts tab to confirm the precise file the tracker is looking for actually exists under the release the event reported. The sentry-cli sourcemaps explain <event-id> command does the same diagnosis from the terminal.

And when you just need to read one trace right now, without any of this pipeline: paste the minified stack and the .map file into the deminifier tool. It runs the decoding locally in your browser and hands you readable code in seconds.

Edge case: Cloudflare and edge Workers

Edge runtimes add their own wrinkle. Workers are always bundled, so traces point at something like worker.js:1:24503, and there's no filesystem to load maps from at runtime—you must upload at build time. The naming conventions matter, too: V8 isolates are particular about worker.js-style source fields, so your uploaded artifact's name has to match what the isolate reports. Short-lived, frequently-redeployed Workers also mean you generate a lot of maps, which is where per-event storage limits start to bite. GlitchReplay's flat-rate storage means you upload every map for every deploy without rationing—covered alongside the cost model in our error tracking on Cloudflare Workers guide.

Stop debugging a (main.js:1:4302) at 3 AM. Unminification isn't one step you forgot—it's three links in a chain, and the failing one is always identifiable if you check them in order. Get generation, availability, and association all green, verify with a synthetic error, and your traces read like your source code again. GlitchReplay gives you that on a flat rate that encourages you to upload every map, every time—no per-event surprises, just readable stack traces and the session replay to go with them.

Stop watching your error bill spike.

GlitchReplay is Sentry-SDK compatible, includes session replay and security signals, and never charges per event. Free to start, five minutes to first event.