Handle loading errors & fallback with HtmlWebpackPlugin
Tags: javascript, webpack, webdev, tutorial
Published: 2020-08-23 20:44:53

TL;DR

If you use HtmlWebpackPlugin & sometimes things go wrong while your bundles are loading, [we’ve got you covered](https://github.com/Uzwername/handling-script-loading-errors-HtmlWebpackPlugin).

---

Errors happen.

This is particularly true about programming. However, when errors happen **before** the code of your program even had a chance to execute, this is somewhat surprising & might be extremely tricky to handle.

This is exactly the problem I had to deal with recently, and it seems likely that this might be a very common issue in client-side web apps.

The error I have seen looked like this:
```
Uncaught SyntaxError: Unexpected token '<'
```

After some research, it turned out that the scenario when the error happened was something like this:

+ A user’s browser caches the page on the initial visit to the website. User doesn’t visit the site again until the _day X_
+ We actively develop the website & make releases meanwhile
+ Each new release adds a bundle with a unique hash to the server
+ We store several latest releases on the server, however, since server resources are limited, with each new release coming in, we erase the oldest release
+ It is _day X_ and the user with the cached version of the page happily comes in
+ User’s browser tries to fetch `bundle.[too-old-hash].js` but it doesn’t exist on the server since we already made several deploys and this old release was erased
+ The server subsequently responds with 404 which is an HTML page
+ JS compiler cannot parse HTML & throws `SyntaxError`
+ Our application would normally be rendered with React on client-side but since there is no bundle, the user sees a blank page

So, how do you handle an error that happens due to the fact that your entire application is unavailable? Below I will show a possible frontend-only solution.

If you prefer to code along, you can find [sample repo with all setup but no solution implemented here](https://github.com/Uzwername/handling-script-loading-errors-HtmlWebpackPlugin/tree/initial-state).

## Setup

We use [webpack](https://webpack.js.org/) to compile & bundle our code and [HtmlWebpackPlugin](https://github.com/jantimon/html-webpack-plugin) to generate an HTML page where our application will eventually live.

Our application could be whatever. This article is framework-agnostic.

## Possible solutions & caveats

First of all, logically we will not be able to do anything in the `bundle.[hash].js`, because this file will fail to load & will be unavailable during runtime.

So, what do we do? Well, we could add some inline JS on our page. It will always be present & hence, will be able to do some handling.

Let’s create `src/index.ejs` which is the default place for the template used by HtmlWebpackPlugin to generate the HTML page. By creating this file, we will be able to customize the HTML skeleton of the generated page.

My first naive attempt was to add some inline JS in HtmlWebpackPlugin template to listen for [error event](https://developer.mozilla.org/en-US/docs/Web/API/Element/error_event) on the app’s script tag like this:

`src/index.ejs`:
```HTML
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Handling script loading errors with HtmlWebpackPlugin</title>
</head>
<body>
    <h1 id="content"></h1>
    <script>
    (function(){
        function showErrorPage() {
            // Doesn't matter for now
        }
        
        var appScript = document.querySelector('script[src^="bundle"]');
        appScript.addEventListener('error', showErrorPage);
    })();
    </script>
    <!--
    HTMLWebpackPlugin will insert bundled script here on build.
    It will look something like this:
    
    <script src="bundle.foo12.js"></script>
    -->
</body>
</html>
```

However, this will not work because when inline `<script>`’s code is executed, `<script src="bundle.foo12.js"></script>` does not even exist yet in the DOM because it is located below the inline script tag and it hasn’t been parsed by the browser yet. Ok, let’s wait until the DOM is ready & once it is, let’s do the same and attach the event listener (below I will omit unchanged parts of the document for brevity):

`src/index.ejs`:
```HTML
<script>
(function(){
    function showErrorPage() {
        // Doesn't matter for now
    }

    window.addEventListener('DOMContentLoaded', function() {
        var appScript = document.querySelector('script[src^="bundle"]');
        appScript.addEventListener('error', showErrorPage);
    });
})();
</script>
```

Unfortunately, this will not work either because when the browser sees the plain script tag that tries to load our bundle, it fetches and executes the bundle straight away, then it resumes HTML parsing and once it reaches `</html>` it fires the [`DOMContentLoaded` event](https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event).

Here is what it looks like graphically:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ydm38v71wyndapjx2tkz.png)

In this case, we attach event listener long ago fetching and execution stages are completed and our callback will never be fired.
So, it looks like we come either too early or too late.

At this stage, we could try to check the bundle’s script presence in the DOM with a very short interval or some other brute-force solution of this type.

Luckily, this is not necessary since HtmlWebpackPlugin provides us with everything to implement an elegant & efficient solution.

## Solution

We clearly need to listen for loading events.
However, in order to be able to listen for loading events, we need more control over **when** our bundle starts to load to attach event listeners on time.

Ok, let’s overtake control.

First of all, let’s tell HtmlWebpackPlugin that we don’t want it to inject `<script>`‘s that we can't control into the page.

`webpack.config.js`:
```js
plugins: [
    new HtmlWebpackPlugin({
        inject: false
    })
]
```

Now we don’t have bundle’s `<script>` tag at all, so, our app will never be loaded. That’s no good, but we can create `<script>` tag ourselves using the information HtmlWebpackPlugin provides to us.

`src/index.ejs`:
```js
<script>
(function() {
    function showErrorMessage() {
        alert('Oops, something went wrong! Please reload the page.');
    }
    
    // Paths of all bundles
    var bundlesSrcs = <%= JSON.stringify(htmlWebpackPlugin.files.js) %>;
    for(var i=0; i < bundlesSrcs.length; i++) {
        // Create script tag & configure it
        var scriptTag = document.createElement('script');
        scriptTag.src = bundlesSrcs[i];
        scriptTag.addEventListener('error', showErrorMessage);
        
        // Append script tag into body 
        document.body.appendChild(scriptTag);
    }
})();
</script>
```

If you use a template [HtmlWebpackPlugin will pass a variable called `htmlWebpackPlugin` to it](https://github.com/jantimon/html-webpack-plugin#writing-your-own-templates). Here we access `htmlWebpackPlugin.files.js` which is an array that contains paths of all javascript bundles created by webpack during this run.

This strange construction “`<%= … %>`” is just [Embedded JavaScript templating](https://ejs.co/) syntax for printing information into the document.
In this case, it will be resolved on the build to something like `['bundle.foo12.js']`.

Once we get the array of paths, we can iterate through this array and create a `<script>` tag for each path.
Before inserting the newly created `<script>` into the document, we will attach our error listener to it. This time we attached the listener on time, so, if any error happens, it will be fired.

Note that I’m trying to use ES5-compliant syntax here because this code will not be compiled & will be shipped into the browser as it is.

## Bonus: Insert images in the error message

IRL we probably want to show some nice “error page” instead of a message in browser’s alert box. Probably, we want to show an image on the error page.

There is no problem with it. Our template is flexible enough to make it possible.

First of all, let’s install [file-loader](https://webpack.js.org/loaders/file-loader/) that can handle images.

`terminal`:
```bash
npm install file-loader --save-dev
```

Now let’s tell webpack to use this loader.

`webpack.config.js`:
```js
module: {
    rules: [
        {
            test: /\.(png|jpe?g|gif)$/i,
            loader: 'file-loader'
        }
    ]
}
```

Now we can directly require images inside our `index.ejs` template like this:

```js
<%= require('./path_to_image').default %>
```

Here is the full `src/index.ejs` file.

```HTML
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Handling script loading errors with HtmlWebpackPlugin</title>
    <style>
        html, body, h1 {
            padding: 0;
            margin: 0;
        }
        #bundleLoadingErrorContainer {
            position: fixed;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #FFF;
            text-align: center;
            width: 100%;
            height: 100%;
        }
        .bundle_error_title {
            padding: 0 1.5%;
        }
    </style>
</head>
<body>
    <h1 id="content"></h1>
    <div id="bundleLoadingErrorContainer" style="display: none;">
        <h2 class="bundle_error_title">Oops, something went wrong. Please reload the page.</h2>
        <figure class="photo">
            <img src="<%= require('./assets/bird.jpg').default %>" width="300" height="200" alt="bird">
            <br>
            <br>
            <figcaption>
                Photo by <a href="https://unsplash.com/@photoholgic" target="_blank" rel="external noopener">Holger Link</a> on <a href="https://unsplash.com/" target="_blank" rel="external noopener">Unsplash</a>
            </figcaption>
        </figure>
    </div>
    <script>
    (function() {
        function showErrorMessage() {
            document.getElementById('bundleLoadingErrorContainer').removeAttribute('style');
        }

        var bundlesSrcs = <%= JSON.stringify(htmlWebpackPlugin.files.js) %>;
        for(var i=0; i < bundlesSrcs.length; i++) {
            var scriptTag = document.createElement('script');
            scriptTag.src = bundlesSrcs[i];
            scriptTag.addEventListener('error', showErrorMessage);

            document.body.appendChild(scriptTag);
        }
    })();
    </script>
</body>
</html>
```

## Conclusion

Hope it was helpful! You can find all the code of the final version in [this repo](https://github.com/Uzwername/handling-script-loading-errors-HtmlWebpackPlugin).

Now, we’re all done and you can test how error handler works using not so well-known [Chrome’s request blocking feature](https://developers.google.com/web/updates/2017/04/devtools-release-notes#block-requests).