Better errors handling for ES/Typescript classes

Better errors handling for ES/Typescript classes

  • 2018-08-29 07:42 AM
  • 325

Better errors handling for ES/Typescript classes , 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.

Writing a custom catch decorator.

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. ⏬

You should “try"

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.

  • We make an API request and wait for a response (token)
  • If there are no errors → handle response
  • If an error is catched in loginUser method → go to catch block and handle error

This 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.

Vue instruments ⛏

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:

Chrome DevTools console

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. ⏬

Let’s catch 🎣

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:

  • target — class, where this decorator is actually used;
  • name — method/property name to which this decorator is applied;
  • property descriptor—specific object that describes the property’s attributes.

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

Conclusion

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.

Shared with ❤️ in Bit’s blog

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.

Learn more

Angular 2 Crash Course with TypeScript

Projects in HTML5

Angular 2 and NodeJS - The Practical Guide to MEAN Stack 2.0

Mastering HTML5 Canvas

Projects In ReactJS - The Complete React Learning Course