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('bad-component', {
template: `<button @click="badOne">Click me</button>`,
methods: {
badOne: async function() {
var response = await fetch("https://non-existent-url");
var data = await response.json();
console.log(data);
}
}
});
var vm = new Vue({
template: `<div>
<h2>My application</h2>
<bad-component></bad-component>
<div v-if="error" class="app-error">
{{ error }}
<span class="close-button" v-on:click="removeError">✕</span>
</div>
</div>`,
data: {
error: "",
errorEl: null
},
errorCaptured: function(err, component, details) {
var _self = this;
_self.error = err.toString() || "Unexpected error has occured!";
setTimeout(function() {
_self.errorEl = component.$el;
if (!_self.errorEl || _self.errorEl.nodeName=="#comment")
_self.errorEl = component.$parent.$el;
_self.errorEl.classList.add("error-frame");
}, 0)
},
methods: {
removeError: function() {
if (!this.error)
return;
this.error = "";
this.errorEl.classList.remove("error-frame");
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.