Source maps are the bridge between your beautiful development code and the mangled, minified production bundle. Learn how they work, how to configure them, and how to debug production errors without exposing your source code.
Every modern web application goes through a build pipeline: TypeScript is transpiled to JavaScript,
ES2026+ features are downleveled, JSX is converted to React.createElement calls,
styles are extracted and optimized, and the whole thing is minified into a single
(or few) bundles. The result is efficient but completely unreadable.
When a production error occurs, your minified bundle gives you stack traces like:
TypeError: Cannot read properties of undefined (reading 'id')
at e (main.a1b2c3.js:1:48732)
at t (main.a1b2c3.js:1:48901)
at Object.r (main.a1b2c3.js:1:49015)
Those column numbers point to positions in the minified file — useless for debugging. Source maps map each position in the minified output back to the original source file, line, and column. With a source map, the same error becomes:
TypeError: Cannot read properties of undefined (reading 'id')
at UserProfile.render (src/components/UserProfile.tsx:42:15)
at renderWithHooks (src/react-dom/ReactFiber.ts:1560:22)
That's the difference between shooting in the dark and having an exact location and call stack. In this guide, I'll cover everything you need to know about source maps — from the internals of VLQ encoding to production deployment strategies.
Before diving into configuration, it helps to understand what's actually in a .map
file. Source maps are defined by the Source Map Specification (version 3),
originally designed at Google and now maintained as a community standard.
A typical source map file looks like this:
{
"version": 3,
"file": "main.a1b2c3.js",
"mappings": "AAAA,SAASA,EAAUC...",
"sources": [
"webpack:///src/index.ts",
"webpack:///src/components/App.tsx"
],
"sourcesContent": [
"import React from 'react';\\n...",
"export function App() {\\n..."
],
"names": ["require", "exports", "module"],
"sourceRoot": ""
}
Key fields explained:
The mappings string is a space- and comma-separated sequence of
Variable-Length Quantity (VLQ) Base64 encoded values. Each segment maps
one position in the generated file to a position in the original source. A single segment
encodes up to five fields:
sources array, relative)names array, optional, relative)All positions are stored as relative offsets — each segment stores the difference from the previous segment, not absolute positions. This compression technique keeps source maps small: a production source map for a 200KB bundle is typically 1-2MB rather than the 10-20MB it would be with absolute coordinates.
The VLQ encoding works by representing numbers as Base64 characters (A-Z, a-z, 0-9, +, /) with a continuation bit. Small numbers (0-15) use one character. Larger numbers split across multiple characters. This is the same encoding scheme used in the Source Map library by Mozilla and Google.
Different build tools use different names for similar strategies, but the concepts are universal. Here's how they compare:
| Strategy | Build Speed | Debug Quality | Security | Best For |
|---|---|---|---|---|
| source-map | Slowest ⚠ | Full — lines + columns + sources | Exposes source ⚠ | Staging/QA |
| hidden-source-map | Slow ⚠ | Full — lines + columns + sources | Hidden from browser | Production with error monitoring |
| nosources-source-map | Slow ⚠ | Lines + columns only (no sources) | Safe — no source code exposed | Production, basic debugging |
| cheap-source-map | Fast ★ | Lines only, no column mapping | Exposes source ⚠ | Development |
| cheap-module-source-map | Fast ★ | Lines + loader-source maps | Exposes source ⚠ | Development (recommended) |
| eval-source-map | Fastest ★ | Full — per-module eval + source map | Exposes source ⚠ | Development (fast rebuilds) |
| eval | Fastest ★ | Module-level only (no mappings) | Worst ⚠ | Development, quick iteration |
Each bundler has its own API, but the core options map to the strategies above. Here's how to configure each one.
Webpack uses the devtool configuration option. For production with Sentry:
// webpack.config.js — production
module.exports = {
devtool: 'hidden-source-map',
// Generates .map files without //# sourceMappingURL
// Upload .map files to Sentry, keep off public web
};
// webpack.config.js — development
module.exports = {
devtool: 'eval-source-map',
// Fastest rebuilds with full debug quality
// Ideal for day-to-day development
};
For fine-grained control over which files get source maps (e.g., exclude vendor bundles),
use the SourceMapDevToolPlugin directly:
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.SourceMapDevToolPlugin({
filename: '[file].map',
exclude: ['vendors-*.js', 'runtime-*.js'],
// Only include app code in source maps
include: ['app-*.js'],
}),
],
};
Vite uses build.sourcemap in vite.config.ts. Rolldown
(Vite's new Rust-based bundler, production-ready since early 2026) uses the same options:
// vite.config.ts — production with error monitoring
import { defineConfig } from 'vite';
export default defineConfig({
build: {
sourcemap: 'hidden', // 'hidden' → no sourceMappingURL comment
// Valid values: true, false, 'inline', 'hidden'
},
});
// vite.config.ts — development
import { defineConfig } from 'vite';
export default defineConfig({
build: {
sourcemap: true, // Full source maps for development
},
});
esbuild's API is command-line and programmatic. Source maps are controlled via
the --sourcemap flag:
# esbuild CLI — production with hidden maps
esbuild src/index.ts --bundle --minify \
--sourcemap=external \
--outfile=dist/main.js
# The .map file is written alongside the output
# No sourceMappingURL comment in the bundle
# esbuild — development
esbuild src/index.ts --bundle \
--sourcemap \
--outfile=dist/main.js
esbuild supports: --sourcemap (inline), --sourcemap=external
(separate file, no comment), --sourcemap=linked (separate file with comment),
and --sourcemap=both (inline + external).
When using tsc for compilation, source maps are controlled via
tsconfig.json:
// tsconfig.json
{
"compilerOptions": {
"sourceMap": true, // Generate .js.map files
"inlineSources": true, // Include source in the map
"sourceRoot": "/src", // Base path for sources
"mapRoot": "/maps" // Output directory for .map files
}
}
The real power of source maps is in production error monitoring. Here's how to set up the complete pipeline with the most popular services.
Sentry is the most widely used error monitoring platform with deep source map support. The recommended approach is to upload source maps during your CI/CD build:
// With @sentry/webpack-plugin (v3+)
const SentryPlugin = require('@sentry/webpack-plugin');
module.exports = {
devtool: 'hidden-source-map',
plugins: [
new SentryPlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
release: process.env.RELEASE_VERSION,
// Automatically uploads .map files + sources
include: './dist',
ignore: ['node_modules'],
urlPrefix: '~/',
// Removes the 'dist/' prefix from paths
// so Sentry matches them to stack traces
}),
],
};
// Alternative: sentry-cli (works with any bundler)
# In your CI/CD pipeline
export SENTRY_AUTH_TOKEN=your_auth_token
export SENTRY_ORG=your_org
export SENTRY_PROJECT=your_project
export VERSION=$(git rev-parse HEAD)
# Create a release and upload artifacts
sentry-cli releases new "$VERSION"
sentry-cli releases files "$VERSION" \
upload-sourcemaps ./dist \
--url-prefix '~/'
sentry-cli releases set-commits "$VERSION" --auto
sentry-cli releases finalize "$VERSION"
DataDog Real User Monitoring (RUM) supports source maps for both JavaScript and Android/iOS. Upload via their API or CLI:
# DataDog source map upload
export DD_API_KEY=your_api_key
export DD_APP_KEY=your_app_key
export DD_SITE=datadoghq.com
datadog-ci sourcemaps upload ./dist \
--service=my-web-app \
--release-version=$(git rev-parse HEAD) \
--minified-path-prefix='https://yourdomain.com/assets/'
DataDog matches error stack traces to uploaded source maps by the
release-version and the minified file path. Ensure the
--minified-path-prefix matches the actual URL path where your
assets are served.
In development, browser DevTools handle source maps automatically. But for production
debugging (e.g., on a staging server where you've deployed with source-map
strategy), enable the DevTools source map panel:
sourceMappingURL comment.For advanced configuration, Chrome DevTools lets you set up workspace mapping — linking the served files directly to your local file system so edits in DevTools persist to disk. This is especially useful with source maps because you edit the original source, not the compiled output.
For a broader overview of Chrome DevTools features beyond source maps, see my Chrome DevTools for debugging guide.
Source maps in production are a double-edged sword. They enable debugging but also expose your entire source code to anyone who opens the browser's DevTools.
The hidden-source-map strategy (webpack) or hidden
(Vite/Rolldown) generates the .map file on disk but omits the
//# sourceMappingURL comment from the bundle. This means:
.map file exists on your server (or build artifact) for upload to error monitoring servicesmain.js.map), you should block .map files at the web server levelEven with hidden-source-map, configure your web server to explicitly block access:
# nginx — block .map files
location ~* \.map$ {
deny all;
return 404;
}
# OR: Apache .htaccess
<FilesMatch "\.map$">
Require all denied
</FilesMatch>
# Cloudflare WAF rule (pseudo-config)
- Field: URI Path
- Operator: ends with
- Value: .map
- Action: Block
An alternative: nosources-source-map generates source maps with accurate
line/column mappings but omits the sourcesContent field.
Stack traces show original file paths and line numbers, but the actual source code
is not included. This is a good middle ground for teams that trust their error
monitoring to correlate stack traces without exposing the full source.
The trade-off between debug quality and build speed depends on your workflow. Here are practical recommendations:
Use eval-source-map (webpack) or sourcemap: true (Vite).
The HMR (Hot Module Replacement) experience benefits from fast rebuilds, and
eval-source-map provides the best balance — each module is evaluated
as a separate eval() call with an inline source map, giving you
per-module debugging without the full source map generation cost.
Use source-map (full quality). Staging environments are internal, and
you want maximum debug quality for QA testing. The slower build time (typically
2-3x vs development) is acceptable for CI pipelines.
Maximum debug quality. Upload to Sentry/DataDog. Block .map files on server.
Safe alternative. Line numbers are visible. No source code exposure.
Based on a medium-sized React application (~800 components, 2MB uncompressed):
| Strategy | Build Time | .map File Size | Bundle Size Impact |
|---|---|---|---|
| No source maps | 18s (baseline) | N/A | None |
| eval | 24s (+33%) | N/A (inline) | +15% bundle size |
| cheap-source-map | 28s (+55%) | 1.8 MB | None (external) |
| cheap-module-source-map | 32s (+77%) | 2.1 MB | None (external) |
| eval-source-map | 26s (+44%) | N/A (inline) | +20% bundle size |
| source-map | 45s (+150%) | 3.4 MB | None (external) |
| hidden-source-map | 45s (+150%) | 3.4 MB | None (external) |
A production source map pipeline has three stages: generate, upload, and audit. Here's a complete GitHub Actions workflow example:
# .github/workflows/deploy.yml
name: Build, Upload Source Maps, Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
- name: Install & Build
run: |
npm ci
npm run build # produces dist/ with .map files
- name: Upload to Sentry
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
run: |
VERSION=$(git rev-parse HEAD)
sentry-cli releases new "$VERSION"
sentry-cli releases files "$VERSION" \
upload-sourcemaps ./dist \
--url-prefix '~/' \
--rewrite
sentry-cli releases set-commits "$VERSION" --auto
sentry-cli releases finalize "$VERSION"
- name: Deploy to Server
run: |
rsync -avz ./dist/ user@server:/var/www/app/
# Delete .map files from public web root
ssh user@server 'rm /var/www/app/**/*.map'
# Map files exist on Sentry and build artifacts,
# but NOT on the public web server
The critical step most teams miss: delete .map files from the
public web root after deployment. Even with hidden-source-map,
leftover .map files are a security risk. The build artifacts should
retain them for future debugging, but the web server should never serve them.
If DevTools shows minified code instead of original source, check:
.map file is accessible at the URL indicated by the sourceMappingURL comment.map file is valid JSON (validate with JSON.parse)Sentry matches stack traces by release version and file path. Common causes of failed resolution:
sourcesContent field is missing (Sentry needs it for full resolution)Source maps are often the largest build artifacts. Mitigations:
SourceMapDevToolPlugin with include/exclude filtersSome error monitoring services report the wrong column number because of how different bundlers handle source map generation for certain constructs:
cheap-source-map to source-map to get column-accurate mappingsterser-webpack-plugin, ensure it's configured to preserve original source map dataA well-configured source map pipeline is one of the highest-leverage investments you can make in your frontend infrastructure. It turns "minified error at line 1, column 48732" into actionable stack traces pointing to your exact source code.
Here's my recommended strategy for production applications:
1. Use hidden-source-map for your production build.
This gives you the best debug quality with the least security exposure — the browser
doesn't automatically fetch the map, and you control exactly who can access it.
2. Integrate source map uploads into your CI/CD pipeline. Automatic uploads to Sentry or DataDog ensure that every release has debuggable stack traces. It takes 15 minutes to set up and saves hours of production debugging.
3. Block .map files at the web server level.
Even with hidden source maps, a security audit will flag any .map
files accessible on your public web server. Deny all .map access.
4. Audit your source map exposure regularly.
Run a simple curl check: curl -sI https://yourdomain.com/main.js.map.
If it returns 200, your source code is exposed. Make this part of your CI/CD
security scanning.
For more on debugging production JavaScript applications, see my guide on Chrome DevTools for debugging. For a deeper look at JavaScript performance optimization, the ES2026 JavaScript features guide covers the latest language improvements.
Source maps are just one piece of a robust frontend monitoring strategy. I help teams set up complete error monitoring pipelines — from build configuration to Sentry/DataDog integration to incident response workflows.
I'm a full-stack developer with extensive experience in production debugging, performance optimization, and frontend infrastructure. Reach out for a free consultation on your application's monitoring and debugging setup.
Need help setting up production source maps, Sentry integration, or frontend debugging infrastructure? I provide free initial consultations.