What this usually means
Most source map failures come from a mismatch between what Webpack produces and what the browser or your loader expects. The most common culprits: using the wrong devtool value for your environment (e.g., 'eval' hides original files), forgetting to configure output.devtoolModuleFilenameTemplate, or having a loader (like babel-loader or css-loader) strip or override source maps. Another frequent cause: the browser's cache serves an old bundle without source maps, or your server sends the wrong Content-Type for .map files (must be application/json). In production, many setups omit source maps entirely for security—if that's intentional, you skip this guide. If not, check that you're not using cheap-source-map or hidden-source-map when you want full original sources.
The first ten minutes — establish facts before touching code.
- 1Open Chrome DevTools → Sources → Page tab. Right-click the file tree and select 'Add folder to workspace'. If you see only bundle files, source maps are not being served.
- 2Check the Network tab for .map requests. Refresh the page, filter by '.map'. If 404 or no requests, Webpack isn't outputting them or the server isn't serving them.
- 3Run `npx webpack --config webpack.config.js --stats=detailed 2>&1 | grep -i 'source map'` to see if Webpack reports any source map errors.
- 4Inspect the generated bundle's last line: `tail -c 100 dist/bundle.js`. You should see a comment like `//# sourceMappingURL=bundle.js.map`. If missing, Webpack didn't attach it.
- 5Check webpack.config.js devtool value. Common broken values: false, 'eval' (only maps to eval, not original files), 'cheap-module-source-map' (only line numbers, no columns). For full original source, use 'source-map' or 'inline-source-map'.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchwebpack.config.js – devtool property, output.devtoolModuleFilenameTemplate, and any custom plugins that might strip maps (e.g., TerserPlugin with sourceMap: false).
- searchbabel.config.js – ensure sourceMaps: true if you override presets.
- searchPackage.json – scripts that set NODE_ENV=production may override devtool via mode.
- searchBrowser DevTools Network tab – filter by .map to confirm files are served and not 404.
- searchServer config (nginx, Apache, CDN) – Content-Type must be application/json for .map files, and they must be publicly accessible if not using hidden-source-map.
- searchOutput directory (e.g., dist/) – check if .map files exist alongside bundles.
- searchCI/CD pipeline config – sometimes build steps delete .map files or set NODE_ENV=production which changes Webpack mode.
Practical causes, not theory. These are the things you will actually find.
- warningdevtool set to 'eval' or false – the most common mistake; 'eval' provides fast rebuilds but hides original files in DevTools.
- warningMode set to 'production' overrides devtool to false or 'nosources-source-map' – check that you're not using mode: production with an explicit devtool that production ignores.
- warningTerserPlugin or other minimizers configured with sourceMap: false in production – they strip source maps after Webpack generates them.
- warningMissing output.devtoolModuleFilenameTemplate – causes paths to be absolute or wrong, especially in Docker or CI environments where file paths differ.
- warningBabel-loader or css-loader with sourceMap: false – these loaders can drop source maps from upstream transforms.
- warningBrowser caching old bundles – DevTools might load a cached version without source maps; hard refresh (Cmd+Shift+R) often fixes this.
- warningServer not serving .map files – e.g., nginx location block blocking .map access, or S3 bucket policy denying public read.
Concrete fix directions. Pick the one that matches your root cause.
- buildSet devtool to 'source-map' for development – this gives full original files and line/column mapping. For production, use 'hidden-source-map' if you need maps for error monitoring but don't want to expose them to users.
- buildAdd output.devtoolModuleFilenameTemplate: 'webpack://[namespace]/[resource-path]?[loaders]' – this normalizes paths so they work across environments.
- buildEnsure TerserPlugin has sourceMap: true in production if you want source maps: `new TerserPlugin({ sourceMap: true })`.
- buildIf using babel-loader, set sourceMaps: true in your Babel config or pass `?sourceMaps=true` in the loader query.
- buildClear browser cache and force reload: Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows). In Chrome DevTools, go to Network tab and check 'Disable cache' while DevTools is open.
- buildFor server-side fixes, add a Content-Type header for .map files: in nginx, add `types { application/json map; }` or use `default_type application/json`.
A fix you cannot prove is a guess. Close the loop.
- verifiedOpen Chrome DevTools → Sources → Page tab. You should see the original source files (not just the bundle) with correct file names and line numbers.
- verifiedSet a breakpoint in an original source file, refresh, and verify execution stops at the expected line.
- verifiedIn the Network tab, confirm .map files are requested and return 200 with Content-Type: application/json.
- verifiedRun `npx webpack --config webpack.config.js --stats=detailed` and look for 'source-map' in the stats output. Verify that the number of source maps matches the number of entry points.
- verifiedUse the source-map-visualization tool: `npx source-map-visualization dist/bundle.js.map` to see the mapping coverage.
Things that make this bug worse or harder to find.
- warningUsing 'eval' in development because 'it's faster' – you'll lose original file names and breakpoints will often fail.
- warningSetting devtool in webpack.config.js but having it overridden by a webpack-merge or mode: production without merging devtool correctly.
- warningForgetting to update the devtool when switching between development and production – use webpack-merge with mode-specific configs.
- warningAssuming .map files are automatically excluded from production deployments – you need to explicitly configure your server or CDN to block them if you don't want public maps.
- warningIgnoring loader-specific source map settings – if a loader like css-loader has sourceMap: false, you'll get no source maps for that file type.
- warningRelying on inline source maps in production – they bloat the bundle size and still expose source, use hidden-source-map instead.
Source maps mysteriously pointing to wrong files after switching to Webpack 5
Timeline
- 09:15Deploy new feature to staging; developer reports breakpoints in DevTools land on wrong lines.
- 09:30Check Network tab – .map files load fine. Open Sources tab: file tree shows original files but breakpoints in `App.tsx` jump to `index.tsx`.
- 09:45Compare webpack config with working production config – only difference is we added `output.devtoolModuleFilenameTemplate` for Docker builds.
- 10:00Temporarily remove `devtoolModuleFilenameTemplate` – breakpoints work again. The template had a typo: `[resource]` instead of `[resource-path]`.
- 10:15Fix template to `'webpack://[namespace]/[resource-path]?[loaders]'`. Rebuild and confirm breakpoints hit correct lines.
- 10:20Add test to CI that verifies source map integrity using `source-map` package.
- 10:30Deploy fix to staging; all breakpoints work. Document the issue in the team's debugging wiki.
I was called in because our staging environment had broken source maps after a routine dependency upgrade. The developer said breakpoints weren't hitting the right lines: clicking on line 42 in `App.tsx` would pause on line 18 of `index.tsx`. The first thing I did was check the Network tab – the `.map` files were being served, so the issue wasn't a missing file or wrong MIME type. I opened the Sources tab in DevTools and saw the original file tree, which was promising. But when I clicked on a file, the source displayed looked correct – yet breakpoints were off. That's when I knew it was a mapping issue, not a loading issue.
I compared our webpack config with the production config (which worked). The only difference was that we had added `output.devtoolModuleFilenameTemplate` to fix an issue with Docker build paths. I had copied the template from an old Webpack 4 guide and used `[resource]` instead of `[resource-path]`. In Webpack 5, `[resource]` returns a different format, causing the source map to refer to the wrong file path. The mapping was still valid, but the paths were mismatched, so DevTools would index the files incorrectly.
I removed the template, and breakpoints worked immediately. Then I crafted the correct template: `'webpack://[namespace]/[resource-path]?[loaders]'`. This normalized the paths across environments. I also added a CI step that runs `source-map` package's `verify` method on the generated source map to catch similar issues early. The lesson: always validate source maps after any config change, especially with path-related templates. A single character typo can cost hours of debugging.
Root cause
Incorrect `output.devtoolModuleFilenameTemplate` using `[resource]` instead of `[resource-path]` in Webpack 5, causing source map paths to point to wrong files.
The fix
Changed `devtoolModuleFilenameTemplate` to `'webpack://[namespace]/[resource-path]?[loaders]'` and validated with the source-map package.
The lesson
Source map templates are sensitive to Webpack version syntax changes. Always test breakpoints in DevTools after any config change, and consider adding automated source map verification to CI.
Webpack's `devtool` option offers a bewildering array of values: 'eval', 'cheap-eval-source-map', 'cheap-module-eval-source-map', 'eval-source-map', 'cheap-source-map', 'cheap-module-source-map', 'inline-cheap-source-map', 'inline-source-map', 'source-map', 'hidden-source-map', 'nosources-source-map', and false. Each makes a different trade-off between rebuild speed, bundle size, and source mapping fidelity. For development, 'eval-source-map' is often recommended because it provides full original source with fast rebuilds, but it uses `eval()` which can have CSP issues. For debugging, I prefer 'source-map' in development despite slower rebuilds – it gives the most accurate mapping. In production, use 'hidden-source-map' if you need source maps for error monitoring (Sentry, etc.) but don't want users to see them. Avoid 'eval' in development – it maps to eval'd code, not original files, and breakpoints often fail.
A common pitfall: setting devtool in a shared config that is later overridden by mode. Webpack's production mode defaults devtool to false or 'nosources-source-map' (depending on version) unless explicitly set. If you use `webpack-merge` with a production config that sets mode: 'production', it will override your devtool. Always verify the final config using `npx webpack --config webpack.config.js --stats=detailed | grep devtool`. Also, be aware that some loaders like `css-loader` and `sass-loader` have their own `sourceMap` option that must be enabled for their source maps to propagate.
A standard source map is a JSON file with fields: `version` (always 3), `file` (output file), `sources` (original files), `sourcesContent` (original sources, optional), `names` (variable names), and `mappings` (a VLQ-encoded string mapping lines/columns). If `sources` contains absolute paths like `/home/user/project/src/App.tsx`, that will break in other environments. This is where `output.devtoolModuleFilenameTemplate` comes in – you can normalize paths to be relative or use a custom protocol like `webpack://`. I often use `'webpack://[namespace]/[resource-path]?[loaders]'` which produces paths like `webpack://myapp/src/App.tsx?babel-loader`. Browsers understand these custom protocols and will display files in the Sources tree under the 'webpack://' section.
To debug a source map, you can use the `source-map` npm package. Run `npx source-map dist/bundle.js.map` to see its contents. You can also use the `verify` method to check that every mapping points to an existing source. In CI, add a step like: `node -e "const sm = require('source-map'); const raw = require('./dist/bundle.js.map'); const consumer = await new sm.SourceMapConsumer(raw); consumer.eachMapping(m => { if (!m.source) throw new Error('Missing source'); }); console.log('Source map valid');"`.
If your source map files are not being loaded by the browser, check the server. The browser makes a separate HTTP request for each `.map` file (as indicated by the `//# sourceMappingURL=` comment). The server must respond with `Content-Type: application/json`. If your server returns the wrong type (e.g., `application/octet-stream`), Chrome may fail to parse it. For nginx, add: `types { application/json map; }` or use `location ~ \.map$ { default_type application/json; }`. For Apache, add `AddType application/json .map`. For cloud storage like S3, set the Content-Type metadata on upload. Also ensure the `.map` files are publicly accessible (unless using `hidden-source-map`, which omits the comment). A common mistake is to have a `.gitignore` that ignores `.map` files, but your build script copies them – check your deployment artifact.
Another server issue: CORS. If your source maps are on a different domain (e.g., CDN), the server must include `Access-Control-Allow-Origin: *` header. Chrome DevTools will show a CORS error in the console if the source map request is blocked. To verify, open the Network tab and check the response headers of the `.map` request.
When using Babel with Webpack, the `babel-loader` can generate its own source maps based on the Babel transform. If you disable source maps in Babel (`sourceMaps: false`), but Webpack expects them, you'll get no mapping for files that go through Babel. Always set `sourceMaps: true` in your Babel config or pass it via loader options: `{ loader: 'babel-loader', options: { sourceMaps: true } }`. Similarly, TypeScript's `ts-loader` or `@babel/preset-typescript` need `sourceMap: true` in tsconfig.json. For CSS, `css-loader` has its own `sourceMap` option (defaults to true in development mode). If you use `mini-css-extract-plugin`, it also has a `sourceMap` option. The chain must have source maps enabled at every step.
I've seen cases where a custom loader or plugin (like `string-replace-loader`) strips source maps because it doesn't pass them through. When writing custom loaders, always call `this.callback(null, content, map)` to propagate the source map. If you're using a third-party loader that doesn't support source maps, consider switching or patching it. Use Webpack's `source-map` devtool to confirm that the final map includes mappings for all original files. If a file is missing, it's likely that one of the loaders in the chain dropped it.
Frequently asked questions
Why do my source maps work in development but not production?
This is almost always because production mode sets devtool to false or 'nosources-source-map' by default. If you want source maps in production, you must explicitly set devtool (e.g., 'hidden-source-map') and ensure TerserPlugin has sourceMap: true. Also check that your production config doesn't override devtool via webpack-merge. Another cause: your server might block .map files in production for security reasons – check your web server or CDN configuration.
I see 'Source map error' in the browser console. What does that mean?
This error usually means the browser tried to load a source map but failed. Common reasons: the .map file returned a 404, the server returned the wrong Content-Type (needs to be application/json), or the source map JSON is malformed. Open the Network tab, find the .map request, and check its response. If it's a 404, the file wasn't generated or isn't in the expected location. If it's a 200, check the Content-Type header. You can also open the .map URL directly in a new tab to see if it's valid JSON.
What's the difference between 'source-map' and 'hidden-source-map'?
Both produce a separate .map file. The key difference: 'source-map' adds a `//# sourceMappingURL=` comment at the end of the bundle, telling the browser to load the map. 'hidden-source-map' does NOT add this comment – the map file exists but is not referenced. This is useful for production when you want to send source maps to an error monitoring service (like Sentry) but don't want to expose them to end users. You can upload the .map files to Sentry separately. For development, use 'source-map'.
My source maps show only the bundle, not individual files. Why?
This happens when the devtool value maps to a single file instead of individual modules. For example, 'eval' maps everything to an eval statement with a single source URL. Use 'source-map' or 'eval-source-map' to get individual files. Also check that you're not using a plugin that merges all sources into one (like some custom plugins). If you use `cheap-source-map`, it only provides line-level mappings and may not show original file names in the Sources tree – switch to 'source-map' for full details.