Making a program work for the happy path is not always easy, but given enough time I believe pretty much anyone could do it. When a professional takes on the task however they will make it work for more than just the happy path, and do it with code that is easy to debug, and easy for others to understand and change. Since so much of what we end up dealing with are exceptional flows, we need a concise way to deal with them. Fortunately we have the aptly named exception pattern.

When the pattern is used well it is almost invisible, and yet we should be thinking about it all the time. Sample code and simple apps often show exception handling as rote boilerplate that writes out stack traces and swallows errors. This is not a good example to be setting.

Most modern languages have an exception type and a throw statement. I’ll be using C#/.Net terminology for this post, but the same or similar terms and patterns exist in Java, JavaScript, TypeScript, Python, and many other languages.

Most scripting languages (shell script, Windows batch, and sometimes PowerShell) and some low level languages like C don’t have exceptions. In these cases you have to check return codes every time you call a function or an external application, and it sucks. You don’t have to look far to find scripts filled primarily with error handling code. For the rest of us, there is something much easier and better.

The Exception Pattern

Here is an example of the exception pattern being used well:

1
2
3
4
5
6
7
8
public string GetMagicString()
{
using var reader = new StreamReader("magic.txt");
var magicString = reader.ReadLine();
if (String.IsNullOrEmpty(magicString))
throw new InvalidOperationException("magic.txt did not contain any text");
return magicString;
}

If you don’t already know how exceptions work you can read about them in Microsoft’s documentation.

You may have noticed that no exceptions are being handled in the function, which is the point. This depends on the built-in exception that will be thrown being good enough. This is because I expect the file to be created by the installer in normal deployments. I could wrap this in a try/catch block and do something, but what would I do? If you were about to say “return null” then you lose 10 points. You should always favour exposing problems quickly and clearly rather than ignoring them or pushing them down the line.

If I couldn’t expect the file to exist, if say magic.txt was a special override file that was only created in certain circumstances, I would write the function like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public string GetMagicString()
{
try
{
using var reader = new StreamReader("magic.txt");
var magicString = reader.ReadLine();
if (String.IsNullOrEmpty(magicString))
throw new InvalidOperationException("magic.txt did not contain any text");
return magicString;
}
catch (FileNotFoundException)
{
return DefaultMagicString;
}
}

Notice that I am only catching the specific file not found exception. I could have caught any exception, but that has its own dangers. Another possible exception is the UnauthorizedAccessException. If that happens, I’d rather notify the user than silently ignore the file.

I added a bit of guard code that can throw the InvalidOperationException. Even if it’s an unlikely problem, I prefer to surface problems like this earlier. They typically save time later when debugging. I’m keeping this code in the second example even while I have access to a default value because I think an empty file is more likely a mistake than intentional. It could be intentional for some use case I don’t know about, but it is always easier to ease restrictions than it is to add new ones in a deployed application, so I’d rather be more strict early.

The using var declaration is a relatively new C# feature that generates an implicit using block. It causes reader to be disposed before the function exits, be it successfully or a because of a thrown exception. Because the stream is opening and potentially locking a file, we want to make sure it is cleaned up quickly. Cleaning up resources in all cases is something we should always be mindful of as well, but in most modern languages you can use features like using to make this task similarly invisible.

Global Error Handling

So where do exceptions get handled? Generally you should have an error handler somewhere, as high up the stack as is reasonable. You only have to write it once in one place, and reducing repetition has a lot of advantages. Your error handler can do any fancy stuff you want it to, and you can change the behavior of your error handling without making sweeping changes across your application.

Most of our applications are iterating on some atomic unit of work. Web servers iterate over web requests. Background processors iterate over messages in a queue or scheduled tasks. It makes sense to implement error handlers at the level where these are invoked, especially if there is a good chance that another similar unit of work could succeed.

In a web api the error handler can usually be added as a wrapper around every request through some hook in the framework. Common implementations will log the exception and return a 500-level status code. Fancier implementations will return more nuanced codes depending on the type of the exception (like return a 400 for ArgumentException or anything derived from it). Your web framework of choice probably already does this for you.

Some errors, however, aren’t recoverable. For example, without complicated retry logic, many applications will be in a bad state if their startup logic fails somehow. You might have one global handler for all your startup code, but its behavior should probably be to output some useful diagnostic information and exit. There may be other errors that indicate poor application health beyond startup, but this is often pretty challenging to deal with reliably.

.Net also has a few special exceptions that can’t be caught normally. Most exceptions mean that the operation you attempted failed, but these uncatchables indicate that the runtime environment could now be in a bad state. The dreaded StackOverflowException is an example of this. In these cases exiting and getting restarted is the only safe thing to do. The framework is going to do that regardless of your error handling so you don’t need to add special logic for it.

Throwing Good Exceptions

The best thing to do when you detect unexpected conditions is often throwing an exception. The exception message should clearly describe what the problem is. Remember that you or one of your peers will be reading this message about 6 months from now when a test fails or a bug gets reported. Sometimes I like to include advice about fixing the problem in the exception, but use this sparingly; it’s much less helpful when the advice has become outdated and sends users on wild goose chases.

It sometimes makes sense to create a new exception type for your error cases. I can’t remember where I once read that exceptions should have the same level of abstraction as the interface that throws them… It’s not terrible advice, but I don’t think it’s worth the trouble for most cases. If you’re building a framework it might make sense. If you’re checking for an empty file after reading its contents it may not.

One case where I do create custom exceptions is when I’m writing a unit test. For really trivial stuff (like argument null) you probably don’t need a unit test, but for anything that should be tested, use a custom exception. If you’re asserting on any exception being thrown your test could be marked as passing when it should be failing. If you derive your custom exception from a built-in or another more common exception, your callers won’t normally need to handle your specific type, but they could if they wanted to.

If you are not creating your own type, try to use a specific and appropriate exception type, either built-in or custom. InvalidOperationException and ArgumentException give better hints the developer in the future than a simple Exception.

It’s also a good practice to include the inner exception whenever you generate a new exception in response to a failure lower in the stack. These details can be very helpful if the new exception is ever thrown unexpectedly.

What About Performance?

One common argument against throwing exceptions is that it causes a performance hit. It’s true, it does takes time to capture the stack trace, and allocating a new object on the heap isn’t free. It depends on your circumstance of course, but I think in 99% of scenarios this cost is too miniscule to matter, and certainly far cheaper than the wasted time of developers who can’t find bugs.

If your exceptional case is something you expect to hit often inside a tight loop though, it may actually matter. The TryFunction pattern is the common alternative in .Net. The drawback is that because your return value is typically boolean you can’t easily add new failure cases as safely as you can with exceptions. You could include more information in another output parameter, but that can also impact your callers whenever the list changes. It also means every caller needs to check the function response, or even worse, they can forget to and the app can continue running in a weird state without realizing the call failed.

What About Leaking Security Sensitive Information?

Another argument I’ve heard against good error handling is to prevent leaking information to attackers, but I think this is mostly bad advice too. For any software that is distributed, a malicious user can easily find tools to look inside and see how it’s wired. For server-side code you should at least share the error details with yourself in your own log files. If you need to withhold information from potential hackers then do that in your global error handler.