« Back to home

How to handle errors in Vue.js in a generic way and how to highlight the component that triggered the error.

This article describes a solution to handle all unexpected errors centrally in a Vue.js application, to process and visualize them in an uniform way. Here's how the end result looks like:

Specifically for this purpose, Vue.js has a very handy errorCaptured hook. This is how it works:

Vue.component('bad-component', {
    template: `<div>The bad guy</div>`,
    created: function() { throw new Error('oops!') }
});

var vm = new Vue({
    template: `<div>
        <h2>My application</h2>
        <bad-component></bad-component>
    </div>`,
    errorCaptured: function(err, component, details) {
        alert(err);
    }
});

show(vm);

By the way: code examples are editable.

The hook can be added not only to the Vue instance, but also to any Vue component. Errors are bubbling up through the components to the Vue instance, triggering all errorCaptured hooks on the way. In order to stop propagation, errorCaptured should return false.

Update: Initially, Vue.js was not catching errors in methods, but this was fixed back in December 2018, so I removed a big part of this article that was explaining how to workaround that methods problem.

And this also works if the error happens in a method:

Vue.component('bad-component', {
    template: `<button @click="badOne">Click me</button>`,
    methods: {
        badOne: function() { throw new Error('from method') }
    }
});

var vm = new Vue({
    template: `<div>
        <h2>My application</h2>
        <bad-component></bad-component>
    </div>`,
    errorCaptured: function(err, component, details) {
        alert(err);
    }
})

show(vm)

That's it! Easy, isn't it?

Displaying the error

Now we only need to implement some UI for displaying the error message and highlighting the component where it occured.

Because we're using errorCaptured (as opposed to the global config.errorHandler), we have the context where we can easily display some error message.

So if we add to template something like:

<div v-if="error" class="app-error">{{ error }}</div>

Then we can display it like this:

var vm = new Vue({
    // ....
    data: {
        error: ""
    },
    errorCaptured: function(err, component, details) {
        error = err.toString();
    }
}

Notice we also have component as one of the parameters. And Vue components have $el property that points to the DOM element. Even though it's a bad practice to do something directly with DOM elements when you are using Vue.js, but we're now talking about exceptional situations which shouldn't happen at all (in best case scenario).

So to highlight the component, we can use $el. Here's the naïve code implementing this idea:

errorCaptured: function(err, component, details) {
    this.error = err.toString() || "Unexpected error has occured!";
    component.$el.classList.add("error-frame");
}

By testing this in practice, I was able to discover some cases when $el is not yet available when errorCaptured is fired (e.g. when error happens in render function), so you need to defer it with setTimeout. Also, if render fails, $el will be empty comment element, so we need to detect that as well and probably highlight the parent component instead.

Finally, we also need to have a close button so that user can clear the error message.

Ok, so here's the final version:

Vue.component(&#39;bad-component&#39;, {
    template: `&lt;button @click=&#34;badOne&#34;&gt;Click me&lt;/button&gt;`,
    methods: {
        badOne: async function() {
            var response = await fetch(&#34;https://non-existent-url&#34;);
            var data = await response.json();
            console.log(data);
        }
    }
});

var vm = new Vue({
    template: `&lt;div&gt;
        &lt;h2&gt;My application&lt;/h2&gt;
        &lt;bad-component&gt;&lt;/bad-component&gt;
        &lt;div v-if=&#34;error&#34; class=&#34;app-error&#34;&gt;
            {{ error }}
            &lt;span class=&#34;close-button&#34; v-on:click=&#34;removeError&#34;&gt;&amp;#10005;&lt;/span&gt;
        &lt;/div&gt;
    &lt;/div&gt;`,
    data: {
        error: &#34;&#34;,
        errorEl: null
    },
    errorCaptured: function(err, component, details) {
        var _self = this;
        _self.error = err.toString() || &#34;Unexpected error has occured!&#34;;
        setTimeout(function() {
            _self.errorEl = component.$el;
            if (!_self.errorEl || _self.errorEl.nodeName==&#34;#comment&#34;)
                _self.errorEl = component.$parent.$el;
            _self.errorEl.classList.add(&#34;error-frame&#34;);
        }, 0)
    },
    methods: {
        removeError: function() {
            if (!this.error)
                return;
            this.error = &#34;&#34;;
            this.errorEl.classList.remove(&#34;error-frame&#34;);
            this.errorEl = null;
        }
    }
});

show(vm)

By the way, if you're using vue-router, it might be also useful to remove error automatically if user leaves current view:

router.afterEach((to, from) => {
    vm.removeError();
});

Conclusion

Vue has a powerful mechanism for catching errors on multiple levels. You can create error boundaries, or catch the errors centrally, and it is possible to visually highlight the component where error has happened.

Comments

comments powered by Disqus