loading...

Sometimes, in the heat of the moment, it's forgivable to cause a runtime exception.

nimmo profile image Nimmo Updated on ・2 min read

Runtime errors suck. But when you're working in JS they're difficult to avoid. Fortunately though, our whole deal is problem solving; so avoid them we do.

For client-side JS this seems totally necessary: We shouldn't subject users to runtime exceptions; we should be giving them appropriate feedback in the event of an error.

But do we always want to avoid runtime exceptions at all costs? I'm not convinced.

In a perfect world, we'd have an equivalent to Elm's compiler in every language. But in the real world, we can save ourselves some time when things actually do go wrong.

Take this, as an example:

import someModule from 'someModule';

const {
  someObject: {
    someFunction,
  } = {},
} = someModule;

Let's assume that our code is being transpiled with Babel before it's deployed. In this instance, if someObject didn't exist in someModule, then this would transpile fine. But at runtime, someFunction would be undefined.

Uncaught TypeError: someFunction is not a function.

It seems like this code is destined to fail for one of our users.

Consider if we'd done it this way instead, without the default value in our destructuring:

import someModule from 'someModule';

const {
  someObject: {
    someFunction,
  },
} = someModule;

Now, if someObject doesn't exist in someModule we'll get a runtime error when trying to transpile instead of after it's been deployed.

Uncaught TypeError: Cannot destructure property `someFunction` of 'undefined' or 'null'.

Not only is this feedback much faster, but it's also going to fail on our machine. This particular example can only even happen in one place in any given file, which improves our ability to locate the problem quickly. With any sort of automated build pipeline in place, this error now can't even possibly make it to production any more. Not bad considering that all we did was remove three characters.

This example isn't indicative of every possible problem we can encounter in JS, of course. But this was a real example that I saw recently. It was the direct result of an over-zealous approach to preventing runtime exceptions: something that the original code didn't even do.

TL;DR: We ought to spend a lot more time thinking about how and where our code can fail, and we should be very careful to consider unintended consequences we introduce by trying to protect ourselves from errors.

Posted on Feb 21 '19 by:

nimmo profile

Nimmo

@nimmo

I'm a software developer based in Newcastle Upon Tyne, England. I've got a wide range of experience in companies of varying sizes and cultures, and in roles of varying degrees of responsibility.

Discussion

markdown guide
 

Awesome! If we see the possibility of a run-time error possibly occurring, then that is the time to discover the appropriate measures to make it impossible. Whether through delegating errors to the compiler (best case) or making the error impossible through design (completely re-arranging the scenario so the error can never occur). Thanks for sharing this gem!

 

Thanks for taking the time to read it. ❤️

Yep, completely agree. I've been guilty in the past of trying hard to avoid my code "falling over", but now I really want to see it fail as quickly as possible if I've actually made a mistake. And crucially, I don't want end users to suffer from my typos. 😄

 

For the longest time, my issue with catching anything ever was silent failures, which I experienced in other people's code.
But now, I just:

  • match errors I want to handle very strictly by message or my custom constructor
  • wrap native errors in my own semantic error types
  • rethrow anything that didn't match
  • use framework error boundaries
  • rethrow in prod after reporting

And life is good.