04. Error Handling Patterns to Prevent Catch-All Bugs // JavaScript Codecasts


Do you get spooked by runtime errors? They can be a pain to deal with, but we’ll
see just how much solid error handling strategies can help in our crusade against if…else
statements on today’s episode of TL;DR, the JavaScript codecast series that teaches
working web developers to craft exceptional software in 5 minutes a week. When you invoke a function, what might happen? Most of the time we get back a simple return
value, but there’s another kind of result a function can produce: an Error. An Error typically makes us think we did something
wrong, but errors are just another feedback mechanism for a program, and unlike returning
a value, throwing an Error has a peculiar superpower: it automatically propagates up
the caller stack — interrupting the caller functions as it propagates — until it’s
caught. This propagation behavior makes throw and
try…catch statements a powerful control flow construct. But handling errors correctly can quickly
turn elegant functions into a hot mess of try…catch statements and nested if…else
statements — exactly the sort of thing we’ve been obliterating in the last few episodes. Today we’re working on a tiny version of
the chatbot we started a couple episodes back that helps outdoor enthusiasts find great
trails to hike. We’ve cut down the chatbot code from the
last couple episodes: it only understands one command, “view hike”, which shows
details about a hike. But sometimes users ask for a hike that isn’t
in the database or their syntax is a bit off. To simulate these edge cases, the viewHike()
function has a few custom error types: it throws a NotFound error if the hike has the
word “lost”, and a ValidationError if the format of the message is off. Like return and continue, throw is a statement,
so to use it in a nested ternary, we wrote a simple helper called raise(). There’s a stage 2 proposal for an expression-friendly
version of throw in the works, but until it lands it’s easy enough to make our own. So all told, the viewHike() function can result
in one of two things: a return value, or a thrown Error. Our chatbot is terse, but it already has some
issues. We definitely don’t want the chatbot to
blow up and stop running if a NotFound error is thrown, so let’s wrap the call with a
try…catch statement to instead return a safe fallback message. Wait, why is our chatbot always responding
with “No such hike” now? That first command definitely worked before. Let’s comment out the try…catch statement
to see what’s happening. It looks like we were swallowing a ReferenceError. Well that would be a horrible bug to deploy
to production! We just made the cardinal mistake of error
handling: a catch all. The try…catch statement will swallow any
error — including errors we didn’t mean to catch. It may sound obvious now, but just about any
open source framework you’ve used probably has a catch-all bug in the codebase, from
frontend frameworks like Ember.js to backend libraries like Passport and Jekyll. A catch-all ranks in the top 5 most frustrating
bugs a library can make because it suppresses important errors unrelated to the library
that the developer would otherwise see in the logs. So it’s up to us to whitelist the type of
error we want to handle, and otherwise rethrow it. Since we made custom error subclasses, we
can use the instanceof operator to guarantee we’re catching an error we can handle. Otherwise, we’ll rethrow it. To rescue a ValidationError, we add another
else-if case. The chatbot is behaving well and not blowing
up, but handling an error correctly looks awful. We definitely can’t leave these checks out,
but a try…catch is a branching construct just like an if…else, so these are essentially
nested, cascading if…else statements all over again. And we’ll have to repeat this boilerplate
each time we need to handle an error correctly. It really doesn’t seem like custom errors
are making our code any better — in fact, it seems to be getting much worse! That’s why you should never be too quick
to sprinkle custom errors throughout your codebase. Because throw statements are fundamentally
a control flow construct, they can often fight against everything we’ve been working towards
in the previous episodes. So when, if ever, should you use custom errors? Well, I prefer the alternative name “Custom
Exceptions” because it tells us exactly when to use them: for unusual, exceptional
cases that most of our codebase shouldn’t care about, like a NetworkError. These are cases that one or two functions
in the codebase will handle with the same response: on the backend, a NotFound error
thrown from any route should just generate a 404 response. Used sparingly, custom exceptions can actually
eliminate branching logic: since the rest of our functions can assume the happy path,
they don’t need an if…else statement to check for an unusual return value, like a
null check. So a custom exception is worthwhile when it
eliminates edge cases and if…else statements from calling functions, and throwing custom
exceptions makes sense when the function would blow up anyway with a useless generic runtime
error, like a TypeError. Let’s see if we can find an error handling
solution that cuts down if…else statements and common typos. Throwing an error triggers an early exit,
even from a catch clause. Let’s shuffle the error checking code so
it looks more like a guard clause. Now there’s nothing stopping us from extracting
this entire guard clause into a function! Let’s call it rescue(). Now when using a try…catch, we just need
to make sure we precede the catch code with rescue(). This behaves much better than what we started
with, and it only added one line to our naive catch-all version. Unfortunately, we can’t just stack invocations
of rescue(), so how do we also handle a ValidationError? Hang tight and we’ll address this problem
on the next episode of TL;DR. Till then, search for try…catch statements
in your codebase and enforce good error handling practices with rescue(). That’s it for today, you can get a transcript
of today’s episode and catch up on other ways to craft exceptional code at JonathanLeeMartin.com/TLDR,
and if you want to keep leveling up your craft, don’t forget to subscribe to the channel
for more rapid codecasts on design patterns, refactoring and development approaches.

Add a Comment

Your email address will not be published. Required fields are marked *