In general, errors handling in JavaScript is not a very complicated topic, but it is very useful. When different errors occur during your application’s life, break it and leave the user bewildered by what is happening. 😵
If you want to protect a part of your code from possible errors, you can just run and test a block of code for errors and, if they occur, handle them and notify user with some message.
All of this can be done using try/catch
blocks or .catch
method of Promise
objects. So far so good. In this post, we’ll dive in to bringing theory to practice with error handling in JavaScript applications. Let’s get started.
This post assumes basic-decent background in JavaScript.
All next examples will be shown in case of Vue class components, as typical errors handling situations. But it can be used with any ES/Typescript classes!
Moving on. ⏬
Most tutorials regarding error handling in Vue components (usually errors are displayed as “toast” messages to the user) will show an implementation that looks something like this (login example):
@Component
class LoginPage extends Vue {
// ...
async loginUser() {
try {
const token = await api.loginUser(this.email, this.password)
handleLogin(token)
} catch(error) {
Toast.error(error.message)
}
}
}
asyncAwaitPromise.ts
Don’t know what is @Component
? It’s a decorator, which allows to use Vue component as class (read more_).
The above implementation is correct and looks great.
loginUser
method → go to catch block and handle errorThis looks easy 😎.
But, what happens when your application is larger that a small tutorial and it contains a lot of API requests and other stuff which can throw errors?
Well, it will look like this:
@Component
class App extends Vue {
// ...
decodeData(data) {
try {
this.decodedData = atob(data) // can throw DOMException
} catch(error) {
Toast.error(error.message)
}
}
async getData() {
try {
this.data = await api.getData() // can throw Error
} catch(error) {
Toast.error(error.message)
}
}
}
appComponent.js
@Component
class LoginPage extends Vue {
// ...
async loginUser() {
try {
const token = await api.loginUser(this.email, this.password)
handleLogin(token)
} catch(error) {
Toast.error(error.message)
}
}
}
loginComponent.ts
As you can see, alot of try/catch
blocks make for some pretty code ugly, imperative and repetitive code (the same errors handling using toast notifications).
It’s better to make your code more declarative, describing what the program must accomplish, rather than just describe how to accomplish it.
So, we should find a way to hide this try/catch
logic and find a more elegant solution to handle errors in our Vue.js app. Let’s give it a go.
If you open Vue’s documentation, you can find a global options property named errorHandler
, to which a handler for uncaught errors can be assigned. This should be the perfect option for us, so let’s try:
// main.js
Vue.config.errorHandler = function (error) {
Toast.error(error.message)
console.warn(error.message)
}
// App.vue
@Component
export default class App extends Vue {
async created() {
const data = await api.getData() // throws Error
// ...
}
mounted() {
this.name = this.name.toUpperCase() // throws TypeError
}
}
globalErrorHandler.js
Great! 😃 We removed the ugly try/catch
blocks and moved handling to theglobal errorHandler
method. Let’s start the app:
Hmmm… Error from mounted
hook catched, but the one from created
is not. The reason for this (Vue’s documentation doesn’t tell anything about it) is that the errorHandler
doesn’t catch errors in asynchronous methods (and native event handlers). Internally, Vue runs our created
hook as synchronous, not waiting forcompletion of an asynchronous operation, when an error can actually be thrown.
Moving on. ⏬
During research, I stumbled upon some Java code and saw interesting error handling, natively supported in Java:
class Main {
public static void main(String[] args) throws IOException {
FileReader file = new FileReader("C:\\test\\a.txt");
// ...
}
javaCheckedExceptions.java
Here throws IOException
tells the compiler that some exception can be thrown in this method and should be handled. It would be great to implement something similar in our case.
However, JavaScript doesn’t have a native mechanism which allows us to implement such a functionality. True… but it does have decorators! 😈
A Decorator is a programming pattern, and below you can see one of it’s implementations:
function log(func) {
return function() {
func()
console.log('Function called')
}
}
function getData() { ... }
getData = log(getData)
getData() // in console: Function called
decoratorExample.js
For those who hear about it for the first time (read more), function log
in the example above is a decorator. It takes getData
function as an argument and extends the behavior of the latter function with logging functionality.
Decorator is a structural design pattern that lets you attach new behaviors to objects by placing them inside wrapper objects that contain these behaviors.
But JavaScript has it’s own decorators implementation — a function that is invoked with a prefixed @
symbol, and immediately followed by a class
, parameter, method or property:
// register global handler
catchDecorator.register((error) => Toast.error(error.message))
@Component
class LoginPage extends Vue {
// ...
// catch errors and run global handler
@Catch()
async loginUser() {
const token = await api.loginUser(this.email, this.password)
handleLogin(token)
}
}
catchSample.js
Here, @Catch()
annotation is our decorator. Let’ implement it. Referencing the ES2016 specification, a decorator is an expression which returns function and can take as arguments:
Base decorator:
function Catch(target, key, descriptor) {
const originalMethod = descriptor.value
descriptor.value = async function(...args) {
try {
return await originalMethod.apply(this, args)
} catch (error) {
console.warn(error.message)
Toast.error(error.message)
}
}
return descriptor
}
export default Catch
catchDecorator.js
First of all, we save the descriptor value to originalMethod
. Then rewrite the original value with our version of target function, where we run original method with arguments from wrapper function.
We use .apply
to link the method with an original context(this
). Calling original method wrapped with try/catch
block, and, for now, just show messages on any error. After that, return descriptor back.
Note:
await
method works for both sync and async functions.
Using our custom decorator in a component:
import Catch from './catchDecorator'
@Component
export default class App extends Vue {
@Catch
async created() {
const data = await api.getData() // throws Error
}
@Catch
mounted() {
this.name = this.name.toUpperCase() // throws TypeError
}
}
catchDecoratorTest.js
Let’s start the app:
It works! 🎉 Now synchronous (from mounted
hook) and asynchronous (from created
hook) errors are properly handled and all this works with only one annotation.
You can also use it for regular Promise
without async/await
, it will work too:
@Component
export default class App extends Vue {
@Catch
created() {
return api.getData() // throws Error
.then(data => ...)
}
}
catchDecoratorPromiseTest.js
For now, our decorator has a hardcoded error handler, and this solution is not very flexible. So, let’s create some store where we can register custom handler, in order to give the decorator the possibility to get this handler:
export const catchDecoratorStore = {
handler: null,
setHandler(handler) {
this.handler = handler
}
}
function Catch(target, key, descriptor) {
const originalMethod = descriptor.value
descriptor.value = async function(...args) {
try {
return await originalMethod.apply(this, args)
} catch (error) {
const { handler } = catchDecoratorStore
if (handler && typeof handler === 'function')
return handler()
console.warn(error.message) // default handler
}
}
return descriptor
}
export default Catch
catchDecoratorStore.js
Here we created a catchDecoratorStore
object with asetHandler
method which set the handler to handler
property. Then in catch
block we check if handler exists, if so — run it, otherwise — show default console message.
Now we can use it very similar to how we used Vue global error handler:
import Catch, { catchDecoratorStore } from './catchDecorator'
catchDecoratorStore.setHandler(error => Toast.error(error.message))
@Component
export default class App extends Vue {
@Catch
async created() {
const data = await api.getData() // throws Error
// ...
}
}
catchDecoratorStoreExample.js
Almost finished. It’s now a fully working implementation and you can use a decorator for any method, event handler and lifecycle hook.
But, what if some method needs a separate error handling logic?. In this case we should add an additional functionality to our decorator — the ability to accept arguments, in order to use it like this: @Catch(handler)
.
The updated version:
function Catch(localHandler) {
return function(target, key, descriptor) {
const originalMethod = descriptor.value
descriptor.value = async function(...args) {
try {
return await originalMethod.apply(this, args)
} catch (error) {
const { handler } = catchDecoratorStore
if (localHandler) {
localHandler.call(null, error, this)
} else if(handler) {
handler.call(null, error, this)
} else {
console.warn(error.message)
}
}
}
return descriptor
}
}
catchDecoratorArgs.js
Here the Catch
function takes a handler as an argument and returns another function which is actually a decorator. The next change made here (line 11) is checking if the handler is passed to the decorator, and if so, we call it with an error object and the context (current object, where target method is located).
Our examples context is a Vue component. If the handler didn’t pass, we’ll call a global registered handler and console a message in other case.
With these last changes you can write components like this:
const getDataErrorHandler = (error, ctx) => {
ctx.errorMessage = error.message
}
@Component
export default class App extends Vue {
errorMessage = ''
@Catch(getDataErrorHandler)
async getData() {
const data = await api.getData() // throws Error
return data
}
}
catchDecoratorArgsExample.js
That’s it, you can now use global handler or, for special cases, local handlers. It also works with any ES6/Typescript classes.
You can find all the code from this article here, clone the repository and play with the source code.
🧐 I also wrote a catch-decorator
library with the same concepts as in this article, but with some extended functionality, which you can find on Github and install from NPM:
npm install catch-decorator
Errors handling can be done in many ways, and you should find the best solution for your tasks and context. When a project is not very big, then moving errors handling to a separate makeRequest
function for API response errors will be enough. But, when you use class components and want to create a unified way to handle errors in your app, decorators can be helpful.
I hope this post was useful 🎓. If you have any thoughts or questions, please feel free to respond and comment below! I will be glad to answer 🙂. Thanks.
Components are building blocks. You are the architect.
Imagine all your components organized on the cloud, made discoverable for your team and synced in all your projects. Give it a try, it’s free & open source.
☞ Angular 2 Crash Course with TypeScript
☞ Angular 2 and NodeJS - The Practical Guide to MEAN Stack 2.0
☞ Projects In ReactJS - The Complete React Learning Course
☞ Web Development Tutorial - JavaScript, HTML, CSS
☞ E-Commerce JavaScript Tutorial - Shopping Cart from Scratch
☞ Javascript Project Tutorial: Budget App