Make Your Try and Catch Bullet Proof

WHY WE NEED TRY-CATCH

Exceptions are unwanted event that hinders our code execution. They are all around us and can show up when you least expect them. It’s this uncertainty that makes it a headache for Developers in designing a robust application.

Javascript being a dynamically typed interpreted language may cause exceptions during execution which may be difficult to reproduce during the development stage. Hence Exception handling becomes an important aspect of software development.

To tackle this problem in javascript we have exception handling constructs like try-catch-finally These allow us to wrap our potential code that can throw an error or cause an exception which we can then handle in our catch block.

      try {
        // Some exception prone code
      }
     catch(e) {
      // Handle exception
      }

Now I have been working with JS for the past 3 years now and I have come to this realization that the purpose and ideas behind the usage of try-catch weren’t actually getting fulfilled.

Sure you could wrap your code with try-catch and prevent your code to cause a runtime exception But that still leaves a blind spot in our exception handling scenario...

Let me explain my ideology and reasoning behind this.

UNDERSTANDING THE PROBLEM

It has a very loose syntax when it comes to bubbling up the error towards the caller function. So Even if you make sure that one part of your code will not cause an exception There is no guarantee at the caller's end.

A function is a lazy entity, which means that it will only execute when the function is called not when it is defined. So now the problem becomes evident. Even though you have wrapped your function definition with `try-catch You have no control over how that function will be called And in which environment will it throw an error.

Here the control in case of try-catch is with the function Definition a Not with a function call

So In order to make this try-catch bullet-proof and more robust What we can do is make sure the point where the function is called Handles this exception state for us. Then we can be sure when and how the function will behave and we can dictate the flow of error handling.

This is what I like to call ‘Inversion Of Control’

Also if you are a fan of Declarative syntax like me, you would have noticed that try-catch is imperative and hence we better need an abstraction for that

EXAMPLE

Let me explain how I achieved this goal with a simple example so that you get an actual idea of how it improves the existing solution

Now this is a very simple and common use-case example that I have taken

Say we have our node development server setup and have some configuration settings that we want to read from a file Now we know that Reading From File always has a possibility of throwing an I/O error So we wrap it with try-catch

This is how it will look -

function getPortNumber() {
    try {
        const fileContent = fs.readFileSync('config.dev.json')
        const config = JSON.parse(fileContent)
        return config.port

    } catch (err) {
        return 3000
    }
}

At first glance it might look like normal and perfectly valid code but if you keep in mind the above-mentioned points that we discussed you’ll start to notice the shortcomings.

Let’s come towards the solution.

DEVELOPING SOLUTION

We have 2 possible state paths for our code execution Let’s call it Right and Left

Right -> Makes sure we follow the normal flow of code

Left-> Short Circuits the code at the moment it is taking this branch

Now we know that to handle errors we must take Left whereas by default we go Right

To make this syntax Declarative I defined my Two Custom Chainable Box Types.

If you haven’t read my previous post on how to make these Chainable Data types give it a quick read to understand the code examples given below


const Right = x => ({
    map: f => Right(f(x)),
    fold: (_, g) => g(x),

})

const Left = x => ({
    map: f => Left(x),
    fold: (f, _) => f(x),
})

Next, we need a tryCatch function so that we go on chaining on these different branches in a declarative and pipelined manner. And inside that, we make decide the path we will follow

const tryCatch = f => {
    try {
        return Right(f())
    } catch (err) {
        return Left(err)
    }
}

Last but not least I like to have a separate function for reading from a file So let’s have that -

const readFile = path => tryCatch(() => fs.readFileSync(path))

Now that I have defined all these building blocks and laid the foundation for the solution, It’s time to compose everything

Declarative Bulletproof designing of Try-Catch -


const getPortNumber_ = () =>
        readFile('config.dev.json')
        .map(JSON.parse)
        .map(({ port }) => port)


const resPort = getPortNumber_()
                .fold( () => 8080,  x => x )

console.log(resPort)

Here you can see how I can go on chaining new methods to my getPortNumber_ because of my custom Data type that I have bound to without even having to think that it might blow up and throw an error or how to handle it there.

And finally, when I need to call the function I can chain fold function to it and provide how do I want to handle these 2 states

The first arg to fold is a function that handles the Error / Left case

And 2nd arg is a function that handles the Success / Right case

Thus we implemented what we wanted We have designed an error handling pattern that has control on the function calling side thus giving us a way to write our code however we want and handle error cases during the function calls.

And this is how we achieve a better solution for Error Handling and a bullet-proof declarative tryCatch

Thanks for making it to the end! I hope you learned something new and exciting today and maybe this post will inspire you to come up with your own better version of existing solutions :)