We recently faced a JavaScript error when running our production build process that proved impossible to debug. This was only an issue when running our production build process, the development process worked fine.

We’d just switched our build tools from Webpack to Vite due to the huge improvements it provides to the developer experience, and had faced a few teething issues here and there; so I assumed this was just another one. When we visited our web application the screen was entirely white, opening up the developer tools we found the following error

A stack trace for an obfuscated Javascript error which is impossible to debug
Stack trace for the Uncaught SyntaxError: 59 error

If you search for “Uncaught SyntaxError: 59” you won’t find any meaningful results, if any at all. This is because Vite is minifying, compiling and obfuscating our JavaScript files, resulting in the compiled code being entirely un-readable to us humans. Computers don’t mind though, in fact the results of a minified, compiled and obfuscated source code results in an optimised and speedier experience in the browser.

No results found for a google search of Uncaught Syntax Error: 59
No results found for a google search of Uncaught Syntax Error: 59

After attempting to look into the minified file vendor.2d43e0af.js, I gave up on debugging with source tools (big mistake) and went into a rabbit hole of exploring whether it was an issue with our Vite configuration, specific versions of vendor packages, or our own code. Eventually, I remembered that source maps exist.

A screenshot of JavaScript function that threw the syntax error.
The function within vendor.2d43e0af.js that threw the SyntaxError: 59 and made me cry

What is the Purpose of Source Maps?

The main purpose of a source map is to provide a mapping between any generated or transformed files back to their original source files. In other words, translating the compiled files back to your human-readable code. This gives developers a huge assistance to their debugging via browser developer tools, since now we can actually reference particular lines of files, and perform searches which might actually return results. The MDN Web Docs have a useful overview on using source maps.

The Anatomy of a Good Source Map

A source map file comprises of a simple JSON object which contains an array of data to help dev tools map any generated code back to the source files. The JSON object typically will contain the following properties:

  • A “file” property which defines the file that the sources are mapped to.
  • A “mappings” property which serves as a translator for the browser to map lines between your generated file and your source files.
  • A “sources” property which defines a list of source files which were used to generate the above “file“.

To get a good understanding of how the mappings in source maps work, Tobias Koppers and Paul Irish put together a great visualisation tool.

There are a few different options when generating a source map. The two most common options are generating separate files, or inline implementations; both of which have their advantages.

Separate Source Map Files

Generating a separate source map file is often the default option used by build tools. Typically these build tools will create a source map for each of input files in your project. As mentioned above, the resulting file is a JSON file containing all of the information needed for your browser’s developer tools to create connections between the generated files and your source code.

If you’re generating source maps for a production environment, it’s often recommended to opt for separate files since they are more accurate and provide better support for minification.

Here’s an example Vite generating separate source maps for our project:

A screenshot of Vite generating separate source map files per input file within our project.
ViteJS generates separate source map files per input file

Inline Source Maps

Generating an inline source map will actually take the resulting JSON that a separate file would contain and appends this directly onto the relevant generated file via a DataUrl. This is often recommended for use within the development environment where you might not necessarily be minifying your code, and where the generated bundle size will be smaller; and therefore produce quicker compilation times.

Here’s a small example of Vite generating an inline source map for one of the components in our project:

var t={methods:{toFormData(t,e,o){let a,r=e||new FormData;for(let n in t){Object.prototype.hasOwnProperty.call(t,n)&&t[n]&&(a=o?o+"["+n+"]":n,t[n]instanceof Date?r.append(a,t[n].toISOString()):"object"!=typeof t[n]||t[n]instanceof File?r.append(a,t[n]):this.toFormData(t[n],r,a))}return r}}};export{t as T};
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHJhbnNmb3Jtcy1mb3JtLWRhdGEuN2EzY2E1ZjkuanMiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3Jlc291cmNlcy9qcy9taXhpbnMvdHJhbnNmb3Jtcy1mb3JtLWRhdGEuanMiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGRlZmF1bHQge1xuICBtZXRob2RzOiB7XG4gICAgdG9Gb3JtRGF0YShvYmosIGZvcm0sIG5hbWVzc

How Do I Generate a Source Map Automatically?

Source map generation for your JavaScript files (or even CSS files) will depend which tool you’re using to bundle your files. Here’s a short list to get you started on some of the more commonly used tools.

Note: for each of the examples, once you’ve altered your configuration files you’ll need to re-run your build process to generate the source maps.

Generating a Source Map with Webpack

To generate source maps for Webpack, you can add the following to your webpack configuration file to enable source map generation:

module.exports = {
  ...
  devtool: "source-map",
  ...
}

Generating a Source Map with Laravel Mix

Laravel Mix is a wrapper for Webpack which provides easy to use methods for the most commonly used configurations within Webpack.

To enable source maps in Laravel Mix, you can add the following .sourceMaps() method to your webpack.mix.js file.

mix.js('src/file.js', 'dist/file.js').sourceMaps();

Generating a Source Map with Rollup

To generate source maps for Rollup, you can add the following to your rollup configuration file:

import path from 'path';
export default ({
  input: 'src/main',
  output: [{
    file: 'bundle.js',
    sourcemap: true
  }]
});

Generating a Source Map with Vite

To generate a source map for Vite, you can add the following option to your Vite configuration file:

export default {
  build: {
    sourcemap: true,
  }
}

Source Map Debugging

So, going back to the issue that we initially faced. Once I’d embarrassingly spent over an hour debugging, I figured it had to be an issue with Vite since this was the major change we’d implemented when moving from Webpack. So I started looking through their docs at their config options when I stumbled upon source maps.

🤦‍♂️ Once I’d sufficiently smacked my head enough times, I generated some source maps with Vite via the above configuration. Finally we had something in our dev tools that started to make some sense:

Before
A stack trace for an obfuscated Javascript error which is impossible to debug
An impossible to read stack trace of the SyntaxError: 59 error
After
A stack trace for for the same Javascript error which contains source file code.
A much more legible stack trace of the SyntaxError: 59 error

Taking a look at the beginning (bottom) of the stack trace, app.js:106 (our entry point into the Vue3 application), we can see that the error originates from the following line:

window.app = app.mount("#app");

Effectively, Vue was having difficulty with mounting the application. Working our way up the chain we can see the following snippet from compiler-do.esm-bundler.js

A screenshot of the JavaScript function that threw the error we faced.
The function that threw the SyntaxError: 59 error, now much more legible

Since we can actually read this package code, we can determine that 59 is actually a constant for X_IGNORED_SIDE_EFFECT_TAG. Now we have something that we can search with.

After doing some quick googling around the ignored side effect tag, I found this issue: [Vue warn]: Templates should only be responsible for mapping the state to the UI. Avoid placing tags with side effects in your templates, such as <script>

The above issue explains that you shouldn’t place a <script> tag within the element where you mount your application. Taking a look at our mounting element below, that’s exactly what we had happening:

<body id="app">
  <header-layout></header-layout>
  <body-layout></body-layout>

  <script>
    //
  </script>
</body>

The fix was a frustratingly simple shift to mount the application where we can ensure no <script> elements would turn up.

<body>
  <div id="app">
    <header-layout></header-layout>
    <body-layout></body-layout>
  </div>

  <script>
    //
  </script>
</body>

In summation, with source maps disabled I managed to spend > 1 hour attempting to debug an issue with a 5 second fix, whereas with source maps enabled it took me a couple of minutes to debug.

Should I use Source Maps in Production?

The question of whether you should ship source maps into your production builds really depends on which of the following matters more to you:

  • I want an easier time fixing bugs in production – then ship source maps.
  • I don’t want users seeing my source code – then don’t ship source maps.

Chris Coyier explores this topic further and makes some great arguments for shipping your source maps into production, including the potential for learning benefits within the developer community.

TL;DR

If you’re ever trying to debug an issue with minified code, source maps are essential to providing you with useful and readable stack traces in your dev tools to help with debugging. Trying to debug obfuscated code is like trying to read a book in another language, you have no reference for what any of it means. It’s easier than ever to automatically generate source maps with the array of build tools available.