Problematic Try-Catches in JavaScript
Tags: javascript
Published: 2020-09-14 13:35:14

---
title: Problematic Try-Catches in JavaScript
published: true
date: 2020-09-14 12:54:51 UTC
tags: javascript
canonical_url: https://www.thisdot.co/blog/problematic-try-catches-in-javascript
---

# Problematic Try-Catches in JavaScript

The `try-catch` syntax is a fundamental feature in most programming languages. It allows us to gracefully handle errors that are thrown in our code, and they do so in a way that is familiar to all programmers.

With that in mind, I'm going to propose that they are also highly misused and have a huge impact on the future maintainability of our codebases, not to mention, force us to sometimes implement error-prone code.

The beauty of using the standard `try-catch` syntax is that if we come back to a section of our code using `try-catch`, we immediately know that something in this block of code may throw an error, and we want to ensure that our application doesn't fall over because of it.

Reading the following block of code, we should get the general understanding of what is happening:

```
try {
  const result = performSomeLogic();
  const mutatedResult = transformTheResult(result);
} catch (error) {
  if (!production) {
    console.error(error);
  } else {
    errorMonitoringService.reportError(error);
  }
}
```

We can see that the block of code will perform some logic to get a result, then it will mutate that result. On error, it will log the error to the appropriate location.

## So what's the problem? 🤔

Or rather, what are the problems? Let's look at each in turn!

## 1. Which method is throwing the error?

If we come back to refactor this block of code, we can't tell simply by looking at each method call in the `try` block which method may throw.  
Is it `performSomeLogic()` or is it `transformTheResult(result)`?

To figure this out, we'll need to find where these functions are defined and read through their source to understand which one could potentially throw an error.

Is the function from a third-party library? In that case, we're going to have to go and find documentation on the function, hoping that the docs for the version we are using are still available online, to figure out which function could throw the error.

### THIS IS PROBLEMATIC

It's adding additional time and complexity to understand the section of code, reducing its future maintainability. Refactoring or fixing bugs in this area is more complex already!

## 2. What if **both** methods should throw?

Here comes a new problem! When both `performSomeLogic()` and `transformTheResult(result)` are expected to throw, the `catch` block does not provide a convenient way to differentiate _which_ threw:

```
try {
  const result = performSomeLogic();
  const mutatedResult = transformTheResult(result);
} catch (error) {
  // Did performSomeLogic or transformTheResult throw?
  // How can we find out?
}
```

So, now that both could throw, how do we find out which threw, in the case that we need to handle the errors differently? Do we inspect the error message?

```
try {
  const result = performSomeLogic();
  const mutatedResult = transformTheResult(result);
} catch (error) {
  if (error.message.includes("performSomeLogic")) {
    // Do error handling specific to performSomeLogic
  } else {
    // Do error handling specific to transformTheResult
  }
}
```

### THIS IS PROBLEMATIC

Now we're coupling our code to an error message, which could change over time, not to mention increasing the difficulty in testing this section of code. There's now two branches here we need to test.

Any developer coming to this section of code to maintain it has to ensure that they take into account the differences in error messages to ensure the errors are handled appropriately.

## 3. I need to use `mutatedResult` for another action

Unsurprisingly you may have to use the result you get from a function that could throw to perform another action, similar to the code above where `result` was used to calculate `mutatedResult`.

Let's say you now need to call a new function `updateModelViaApi(mutatedResult)`. Where do you put it?

Inside the `try-catch` after you calculate the mutated result?

```
try {
  const result = performSomeLogic();
  const mutatedResult = transformTheResult(result);
  const response = updateModelViaApi(mutatedResult)
} catch (error) {
  if (!production) {
    console.error(error);
  } else {
    errorMonitoringService.reportError(error);
  }
}
```

Surely not. You're only putting it there because you need access to `mutatedResult` which is within the `try` scope. If you then had to perform more logic with the `response` object, would you also put that into the `try` block?

```
try {
  const result = performSomeLogic();
  const mutatedResult = transformTheResult(result);
  const response = updateModelViaApi(mutatedResult)

  if(response.status === 200) {
      letsDoSomethingElse();
  }
} catch (error) {
  if (!production) {
    console.error(error);
  } else {
    errorMonitoringService.reportError(error);
  }
}
```

### THIS IS PROBLEMATIC

Ok, our `try` block is continuing to grow, and going back to point 1, we are making it more and more difficult to understand what our `try` block is actually doing and further obscuring which function call we are expecting to throw. It also becomes much more difficult to test and more difficult to reason about in the future!

Could we not just move the variable outside the `try` scope? We could:

```
let mutatedResult;
try {
  const result = performSomeLogic();
  mutatedResult = transformTheResult(result);
} catch (error) {
  if (!production) {
    console.error(error);
  } else {
    errorMonitoringService.reportError(error);
  }
}

const response = updateModelViaApi(mutatedResult)

if (response.status === 200) {
  letsDoSomethingElse();
}
```

However, while this does reduce the amount of code in the `try` block, it still presents us with an issue of future maintainability, as well as a potential bug. We've declared a variable outside our `try` scope, without assigning it a value.

If an error is thrown before `mutatedResult` is set, execution will continue and our `updateModelViaApi(mutatedResult)` will be called with `undefined`, potentially causing another issue to debug and manage!

## We see problems, but what's the solution? 🔥

To fully understand how to solve the problems presented, it's important to understand the goal of the `try-catch` syntax.

> Try-catch allows the developer to execute throwable code in an environment where we can gracefully handle the error.

With this in mind, we have to understand that the implementation of this syntax by the language is essentially what creates these issues. If we look at the example above where we moved `mutatedState` outside the `try` scope, we solve an issue, but by doing this we break the functional programming concept of immutable state.

If we think of the `try-catch` block as a function, then we can see this breach of immutable state much clearer:

```
let mutatedResult;
tryCatch();
// expect mutatedState to now have a value
const response = updateModelViaApi(mutatedState); 
```

However, by considering the `try-catch` block as a function, we **can** eliminate the problems we talked about earlier.

Having the `try-catch` logic moved into a function, we:

- create a consistent pattern of running _only_ the throwable code _(Point 1)_
- can handle multiple throwable function calls and handle their individual errors explicitly _(Point 2)_
- don't have to worry about block-scoped variables _(Point 3)_

So how do we transform the `try-catch` into a function?

## Introducing no-try! 🚀

Luckily we don't have to. There is already a library that has done this for us.  
_NOTE: It should be noted this is a library I wrote_

The library is called `no-try` and you can read more about it [here](https://github.com/Coly010/no-try). It will work in a browser environment as well as a node environment.

So what does `no-try` let us achieve?

Let's jump back to our first example and see if we can tackle the problem of Point 1 and refactor it to use `no-try`.

```
const { useTry } = require('no-try');
// You can also use 
// import { useTry } from 'no-try';

const [error, result] = useTry(() => performSomeLogic());

if (error) {
    console.error(error);
}

const mutatedResult = transformTheResult(result);
```

We can now see exactly which method we expect to throw an error, making it easier for any developer coming along afterwards to refactor this logic if they need to.

Admittedly, there's a slight cognitive load added to understand what `useTry` is, as it's not as immediately recognisable as a `try-catch` but from the naming and the usage, it should be pretty self-explanatory.

Can we also solve Point 2? Individually and explicitly handling errors thrown by multiple throwable function calls? Well, yes!

```
const { useTry } = require('no-try');

const [error, result] = useTry(() => performSomeLogic());

if (error) {
    console.error(error);
}

const [transformError, mutatedResult] = useTry(() => transformTheResult(result));

if (transformError) {
    notificationService.showError(transformError);
}
```

Now we can see that both methods may throw an error. We can handle both of these errors individually and without having to write code to figure out which error we're handling, reducing future maintenance.

Finally, tackling Point 3 should now be fairly straight forward. We don't have to worry about block-scoped variables or a `try-catch` block that's getting bigger and bigger as we need to execute business logic. If an error is thrown, we can exit the function before running code that might rely on a successful outcome:

```
const { useTry } = require('no-try');

const [error, result] = useTry(() => performSomeLogic());

if (error) {
    console.error(error);
    return;
}

const mutatedResult = transformTheResult(result);
const response = updateModelViaApi(mutatedState); 

if (response.status === 200) {
  letsDoSomethingElse();
}
```

This is much easier to reason about and it's straightforward to read. We can see what is expected to throw an error, where it is handled, and we aren't placing unnecessary code inside the `try-catch` block due to limitations presented by the language itself.

* * *

_This Dot Labs is a modern web consultancy focused on helping companies realize their digital transformation efforts. For expert architectural guidance, training, or consulting in React, Angular, Vue, Web Components, GraphQL, Node, Bazel, or Polymer, visit [thisdotlabs.com](([https://www.thisdotlabs.com](https://www.thisdotlabs.com))._

_This Dot Media is focused on creating an inclusive and educational web for all. We keep you up to date with advancements in the modern web through events, podcasts, and free content. To learn, visit [thisdot.co](https://www.thisdot.co)._