← All articles

How to stop worrying and love the error

From the helpful syntax error to the dreaded null pointer exception, errors are part of our everyday development experience. Although sometimes misunderstood or mistreated, each with its own quirks and perks, they can become trustworthy companions when handled correctly.

There's a direct correlation between the moment when an error occurs and how much effort it takes to fix the faulty behaviour that caused it. It's the sooner the better: a compile-time error is always preferable over a run-time error. Yeah, taming these beasts starts early, pick your tools and stack right, whenever possible use a statically typed language with a mature compiler/static analyzer + a linter.

In truth, it's the faulty hidden behaviour in your code that we must fight. Errors are just the bearer of bad news! Avoid ignoring exceptions or catch-to-hide them. Handle, log and monitor every single one.

To give you a hint of how important this is, one of Deno's (most probably NodeJS's successor) main selling point is that unhandled exceptions will always break. Think about it, a feature seamingly so small, mentioned in almost every article out there. Swift learned from the mistakes of their forerunners and enforce the error handling at compile time: or in other words, your code won't compile if you don't write the part that handles possible exceptions.

But not all exceptions are equal. Some are recoverable while others are not. Draw a clear line between these types. And not based on some language-inherited rule (e.g. Java), but a conceptual, well-defined border between what should be retried if it fails, what should be logged, what should trigger an alert, what should bring the process down and so on.

This is entierly at our disposal, and should be tailored based on the project's needs: it would be catastrophic to stop a monolith webserver because of a trivial third party service exception, but highly recommended for a CLI or even an easy-to-spin, well designed worker in a microservices architecture. There's a subtle difference between an inherited fatal exception (e.g. our process' memory is corruped beyond hope, all we can do is crash as soon as possible) and a fatal exception by design. Thus, it's absolutely normal for the error boundary to shift during a project's lifecycle.

Exceptions are designed to bubble up the callstack, let them do so. Catching an exception right away after it was thrown should only happen if absolutely needed, for example, to revert a previous operation on failure. And even then, the exception should be rethrown. Handling them early clutters the code making it hard to maintain.

An exception without the stacktrace is about as useful as a spoon without a handle. Having the stacktrace is paramount for debugging.