catchjs

Rental cars, Destiny's Child and error handling with Vue.js

Why you should handle your errors, a story from the trenches

There are at least two reasons why you would want to set up custom error handling in your JS project: One is pragmatic and the other is scary.

For the first case, there might be errors you know will happen, and you just want to adjust something whenever it does. Say you have a component that definitely breaks in very old browsers, and you just want to disable it whenever it crashes for some user of IE9. Fine, set up an error handler to do this.

The scary case is the errors that happen that you don't know about.

Story time: I was going to the Strange Loop conference in St. Louis, and was booking a rental car at the website of a major car rental company. I don't want to embarrass them by saying their name, so lets call them Pudget. After punching in my credit card number, name and street address, the registration form broke. The submit button did nothing. This is where a normal person would give up and use a competitor. However, if you're a hardened programmer with something to prove, this is when you open up the developer console. It turns out the form was silently failing because my street address contained the letter "Ø", and thus would not match with /^[a-z0-9 ]+$/i.

Crashing on the letter Ø will make you lose a small number of Norwegian customers, which might not be a biggest deal in the grand scheme of things. But consider the number of people in the world that write in Cyrillic, Hanzi, Kana, Hangul... Hell, even the Germans like to throw in a little ß once in a while, and Spanish speakers might use the occasional ñ. By restricting to [a-z] as a car rental company, you are rejecting most of the non-English speaking world, which is probably exactly the people who are interested in renting a car when they are in the US.

I've made a huge mistake.'

This is exactly the kind of stupid mistake we've all made, and it was happening in silence. Customers with their credit card in hand were being turned away, and no one responsible knew about it. That is the other reason to set up error handling, to notify you whenever something inevitably breaks, so you can sleep well knowing nothing is breaking without your knowledge.

Right, so how do I handle errors with Vue.js?

Vue has a few fairly simple mechanisms for setting up error handlers. However, the devil is in the details, and it turns out that the straight forward approach will miss certain classes of errors. In the end we'll describe a solution for how you can catch every error happening in a production app.

There are two particularly relevant parts of the Vue.js API that you should be aware of: errorHandler and warnHandler. As you might have guessed, these are for handling errors and warnings, respectively. Simply set these handlers up before initializing your app.

Vue.config.errorHandler = function(err, vm, info) { /*your code*/ }
Vue.config.warnHandler = function(msg, vm, info) { /*your code*/}

Here err is the JavaScript Error object that was thrown, vm is the relevant Vue instance, and info is a string specifying in which part of the Vue lifecycle the error occurred. The same goes for warnHandler, except the first argument msg is a string containing the warning.

What's an error and what's a warning?

Here errors are exceptions that are thrown in JavaScript and not handled, while warnings are for problems that Vue itself detects during rendering. Vue will only produce warnings when in development mode, so setting up code to handle these may be of limited use.

Error boundaries and errorCaptured

Another nice mechanism that Vue provides (as of version 2.5) is that of errorCaptured which allows you to catch errors on the component level. This in turn allows you to implement error boundaries. You can set up errorCaptured when defining your component:

Vue.component('parent', {
    template: '<div><slot></slot></div>',
    errorCaptured: (err, vm, info) => alert('I have a broken child :(');
})
Vue.component('child', {
    template: '<h1>{{ fail() }}</h1>'
})

You can then use these components like so:

<parent>
    <child></child>
</parent>

The parent component will catch the errors of the child component. Note that the errorCaptured will only catch errors in the child components, and not in the component itself. So, the parent will see the faults of its children, but not the faults of itself, just like in your upbringing.

If the errorCaptured function returns false, it will not propagate the error up to its parent component. This allows us to create a generic error stopping mechanism, called an error boundary.

Vue.component('error-boundary', {
    template: '<div><slot></slot></di>',
    errorCaptured: (err, vm, info) => {
        console.log('We have an error');
        return false;
    }
})
Now we have a generic mechanism that we can wrap components in.
<error-boundary>
    <something-that-can-fail></something-that-can-fail>
</error-boundary>

This is like a try-catch for your markup, to catch problems that occur during rendering.

But wait: This will not handle all errors!

errorCaptured, can you handle this?
warnHandler, can you handle this?
errorHandler, can you handle this?
I don't think they can handle this!
Destiny's Child, early advocates of setting up a global error handler for the errors that happen outside of the Vue rendering process.

There's a problem with all of this this: It will not catch all errors. Essentially, it only catches errors that the Vue code has a chance to see. Vue is not a totalitarian framework that wraps itself around everything you do, so there are still errors these handlers will never catch. Like Pokemon, we want to catch them all. Our courage will pull us through. You teach me and I teach you.

Errors happening outside of the Vue rendering process will not be caught. For example, if you bind a function to the click event, and your function creates an error, it will not be caught by any of the mechanisms above. Generally, errors thrown in your code will not be caught by these handlers.

Update: As of Vue 2.6, released February 2019, errorCaptured and errorHandler will capture errors thrown inside v-on handlers. Starting this release, you can also return Promises in lifecycle hooks and event handlers, and have async errors be logged with these mechanisms. This opens a whole other can of worms though, see our article on async/await errors.

How to actually catch every error

In order to actually catch every single error, you need to set up a global error handler in the browser, by assigning a function to the window.onerror property.

window.onerror = function(msg, src, linenum, colnum, error) { /*your code*/ }

This will be called on all uncaught errors. Here msg is the error message, src is the URL to the file in which the error happened, linenum and colnum is the line and character number at which the error occurred, and error is the error object. Be aware though that this API is a mess in terms of browser inconsistencies.

Also, be aware that errors thrown in scripts loaded from another origin will only show up as "Script error.", unless you take care to set up CORS.

Nevertheless, if you take care to deal with these issues, you will have a reliable error handling mechanism that gets called whenever and unhandled error occurs.

How to log every error with CatchJS

If you want to catch every error that occurs client side, and log it persistently to a server, you can achieve this by simply dropping in the CatchJS script.

<script src="https://cdn.catchjs.com/catch.js"></script>

You will need an account with the service, but if you're using Vue.js, you don't need any changes to your code. Simply dropping in this script will set up a global error handler in the browser, and automatically log all errors, along with the telemetry needed to reproduce the circumstances of the error.

Remember, friends don't let friends discard unknown exceptions without logging them first. Stay safe!


If you liked this post, you might like our post on error handling with async/await, or our post on React error handling and error boundaries.