Exceptionless Programming

Handling errors in large applications is a complex and difficult task. Let’s see how we can make it easier through exceptions.

Danny Vernovsky
AT&T Israel Tech Blog
5 min readJun 1, 2020

--

Photo by Florian Olivo on Unsplash

Introduction

At its basics, code is a flow of instructions with forks. It is all what it is actually. Nowadays, however, we write a lot of code that tends to get complicated very fast. So we have tools that help us manage the code like functions, switch..case, etc.. Different programming languages offer different tools to help manage our code.

Another challenge we have is handling errors. Most programming languages we have today have some sort of exception handling mechanism. This mechanism is essential for handling errors we have throughout the execution of our program.

It is important to note that handling code flow and error flow are two very different flows and each has its own purpose, It’s not always clear which flow we need to use in which situation.

This post aims to discuss different scenarios in the life of an application and determine which flow should be leveraged, as well as suggest an alternative way to handle the error flow. To do this let’s first discuss what errors are.

This article use Typescript and Node.js as a base for examples but the same techniques can be applied to almost every programming language, even if the code looks a bit different

Types of Errors

There are three common error types in an application:

Unexpected error

In theory, unexpected exceptions are very hard to come by, as with enough tests and design we can anticipate almost every possible error we can encounter. In reality, we all experienced application crashes so we know this is not true and we can expect some unhandled errors for every application we write

So unexpected errors are ones that we, as developers could not have anticipated or could anticipate but didn’t. One very simple example of such an error can be for example an incorrect value received when it was not anticipated. Let’s suppose we have a function to calculate the tip:

In this example, if we receive one of the values from an external source and we do not check it, it can be a string without us knowing it, although we explicitly describe these values as numbers in our code. This error will rightfully bubble up as an exception until it reaches a global exception handler or crushes the process. I personally prefer to crush the process for reasons we will talk about later in this article.

Expected Fatal Error

Every application has expected errors that if are received making the application execution not relevant anymore. One example can be a DB connection. An application that relies heavily on data received from a database will probably be unusable without a successful connection. For this kind of error, it is also recommended to throw an exception. This is a fatal error, and fatal errors should fail loudly and unless we have a very good reason not to do it.

Expected “Normal” Errors

“Normal” errors are errors we do expect and that can either stop the current operation or change execution flow. However, they are not bad enough to take the whole process down. I would argue here that they are also not bad enough to create exceptions out of them.

So, these errors are the key subject of this article. Let’s begin with an example:

In this example, we are handling messages. Our application can process some messages while others are not relevant. In case of an unknown message received an exception will be thrown to whoever is activating this handler (let's call this code “actor”) and here is where the problems start.

The actor should know in advance (or assume) that an exception can be thrown and therefore catch it. If we would catch the exception in the actor, we should then guess what type of object returned in this exception. As in Javascript, you can throw any type of error and you cannot make different catches based on the error type.

If the actor decides not to catch exception it is even worse because it is impossible to know at what level it will be caught. It will bubble up and will be handled somewhere and if another layer in between will add try..catch in the future it will be caught there instead of possibly altering the flow.

This creates an unstable error handling flow that is prone to fail and potentially will take the whole process down with it. So what can we do?

Possible Solutions

No Error!

One possible solution is not to handle the error at all. Assuming the unknown message does not break the flow, we can just ignore it and just return null. After updating the code it should look like this:

This is better, but still can’t give us information on what happened; we just know that something went wrong.

Unions to the rescue

Another option is to use unions in Typescript. We actually already used unions in our previous example where we said that the result can either be status or null. In this case we can say that it is either status or some other error type we’ve described. function handleMessage(message: Message): Status | Error

This will work but looks a bit dirty since we will need to understand what type of object we’ve received before we will act on it.

Tuples 🙌

If you are familiar with Nodejs you have probably seen how async functions work there. Most of the time, an async function will call a callback with 2 properties, the first is a potential error, the second is the result:

So how can we make it work in our favor? Typescript has support for tuples. Tuples can easily be mistaken for arrays as they behave very similarly. The main difference between them is that their length is predefined, as well as the type of every element in order. To describe a tuple, we need to explicitly define its length and types. So with that, let’s refactor again.

As we can see here, we now only use tuples to return an error and a result at the same time. We also make use of unions to note that both error and result can be null.

But what about async/await?

Glad you asked :) Actually it is very easy to refactor this code to support asynchronous flow. All we need to do is wrap the code with Promise like this:

Please note that: in node.js unhandled promises do not behave (by default) as exceptions, meaning that they do not stop the process. This will change in the future, but, until then to make it behave the same way as normal exceptions do we will need to add a little piece of code as explained here: https://medium.com/@dtinth/making-unhandled-promise-rejections-crash-the-node-js-process-ffc27cfcc9dd

Final Thoughts

So here we have it, “exceptionless programming” approach that leverages typescript union and tuples.

It is important to note that every tool and every method has its own place, and we should not treat this programming method as the law of the instrument. From my experience, when we write multi-layer applications, with services, controller/components, db-accessors we can benefit from this a lot.

There is one downside for this method, because we described both error and result as possibly null, we will need to have two if statements, first to check that there is no error, and the second one to check that result is not empty unless you disable the strictNullChecks flag in typescript.

--

--