Implementing Error Handling that Hopefully Doesn't Suck
Tags: programming, api, architecture, errors
Published: 2022-09-13 06:12:36

Where there is software, there are errors. (Here, we treat _errors_ and _exceptions_ as synonyms, because languages do not agree on the terminology in the first place.)

Some errors are expected. Some are unexpected.

But for whatever the reason, errors can occur, and whether you're writing an application or a library, you should think about your strategy for dealing with errors.

This article goes over the commonly used error handling strategies, and offers advice on how to use each.

# Strategy 1: Error return codes

A very common error handling strategy in structural programming is simply to return an error code from all functions.

There are three common sub-approaches here:

1. Return 0 for success, anything else for failure
2. Return boolean true for success, false for failure
3. Return the actual function result for success, a _sentinel value_ for failure

An example of returning 0 for success is POSIX's `fsetpos`:

```c
int fsetpos(FILE *stream, const fpos_t *pos);
```

An example of returning true for success is Windows' `CreateDirectoryW`:

```c
BOOL CreateDirectoryW(
 [in] LPCWSTR lpPathName,
 [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
```

An example of returning a sentinel value is Windows' `CreateFileW`:

```c
HANDLE CreateFileW(
 [in] LPCWSTR lpFileName,
 [in] DWORD dwDesiredAccess,
 [in] DWORD dwShareMode,
 [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
 [in] DWORD dwCreationDisposition,
 [in] DWORD dwFlagsAndAttributes,
 [in, optional] HANDLE hTemplateFile
);
```

If this function fails, it returns `INVALID_HANDLE_VALUE`, which happens to be -1. Valid handles cannot be negative, so this will never clash. You should only use a sentinel value in this situation where the legal return values do not take up the entire value space.

Since returning a value is not particularly informative if there are multiple reasons an error could have occurred, this sort of API usually comes with an additional function you can call in order to get further details about the error. For this, POSIX provides the `strerror` function, while Windows provides the `GetLastError` function.

Advice for using this style of error handling:

* Don't use it if you're writing in an object-oriented or functional language - users of these languages would generally consider this to be bad form.
* Try to use consistent style for handling errors in all functions. _Don't_ follow the POSIX or Windows examples of choosing this on a function-by-function basis.
* Try to separate your errors into different error return codes so that people have an idea what kind of error happened just from seeing the error code in a log. Some callers will crash on any non-successful return code and not bother to call your separate method to get the explanation.
* If you have to choose, go with 0 for success and other values for error codes, because it gives you the largest possible chance of keeping the API consistent while allowing the maximum number of unique return values for errors.
* Provide a function to get the error message as a string. Allow passing the user's locale into that function, so they can get the message in their language, not yours.

# Strategy 2: Throwing

Many languages implement a style of exception handling where an exception can be _thrown_ (or _raised_) from any stack frame, which can then be _caught_ any number of stack levels away by some other code which signed up to catch it.

Languages like Java separate exceptions into _checked exceptions_ and _unchecked exceptions_. The fundamental principle here is that you use checked exceptions for conditions where the caller **really should be aware** of the possibility of the error, while you use unchecked exceptions where the exception is **essentially unexpected**.

Unfortunately, Java's standard library does an extremely bad job of using the right kind of exception for the right situation:

* _Unexpected_ errors reading from a file or the network typically use _checked_ IOException when it should have used unchecked - things like `FileNotFoundException` make sense being checked exceptions, but general I/O errors because a disk was yanked from the system do not.
* _Expected_ errors parsing a value will typically use an _unchecked_ `NumberFormatException`, `IllegalArgumentException` or similar.

Because Java's standard library has done such a bad job of using this feature, some people consider checked exceptions a bad idea in general. But this is unfair on the feature—in general, checked exceptions are a good idea when used correctly.

If you are writing a library using this sort of exception to relay errors, here is my advice:

* Only put checked exceptions on API methods where the caller really has to pay attention to the error condition at the site they call it, and use them extremely sparingly in general. Consider: _if the caller catches this exception_, is there realistically anything they can do to recover from it? (For example, if they get a checked `FileNotFoundException`, they might want to create the file. But if they get a checked `NetworkOfflineException`, they're going to be annoyed because there is nothing they can do to bring back the network.)
* Use polymorphism. Declare a common base class for your exceptions, but then declare one subclass for each reason they could occur. The caller does not want to have to parse your English exception messages to figure out whether they got an `AuthenticationException` or a `ConnectionException`.
* Group together exceptions which have the same kind of underlying cause, e.g., all authentication exceptions. The caller often wants to display a message for a given category and doesn't care about which of a dozen potential underlying causes were the reason.
* If you're carrying more than a hard-coded message in your exceptions, add methods or properties allowing the caller to get the useful information out of the exception. Again, the caller does not want to parse your messages to get this out.

# Strategy 3: Polymorphic return values

If you're working in a language where things are type-safe, and there is no sensible way to return multiple potential types from a single method, one of your options is to specify the method as returning a base class and then have multiple potential implementations of that class.

For example, you might have the method return a `GetUserResponse`, and then actually implement `GetUserSuccess` and `GetUserFailure` subclasses for that response type.

This is somewhat cleaner in languages such as Kotlin, where you can declare the base class as `sealed`, as the caller can then see the complete list of possible subclasses they have to handle. But there is no way this can be bulletproof if it appears in a public API - a sealed class today could have one more subclass tomorrow, so it gives the caller a false sense of security about the API stability.

Because new subclasses can appear at any moment, I'm not a big fan of this style of exception handling in general, but it can make a lot of sense for simplifying internal application code.

Advice for this type of exception handling:

* If you're using sealed classes, limit that to internal logic in your application and _don't_ expose it to a caller - the reasoning is that the caller's safety is ripped out from under them if you ever add an additional subclass.
* Consider how you will distinguish different possible failures - will you have different subclasses, or just a detailed failure property inside the returned Failed object?

# Strategy 4: `Try` monads

As an alternative to using polymorphism to indicate the outcome, another approach is to use _monads_, which I will loosely equate to _wrapper classes_ for people who don't have a functional background.

The general idea here is that you declare a `Try<T>` class which gets used like this:

```kotlin
fun doSomething(): Try<DoSomethingResult> {
    try {
        // do the thing here
        return Try.of(successfulResult)
    } catch (e: SomeException) {
        return Try.ofFailure(e)
    }
}
```

This comes out very similar to how you would use an `Optional<T>` instead of returning nullable values - you return a `Try<R>` instead of throwing the exception.

The caller then is forced to handle the exception—they have to unpack the Try wrapper to get the actual result out—so you can add convenience methods like `ifSucceeded`/`ifFailed`, methods like `map` which can transform the success condition, methods like `flatMap` which can be used to chain subsequent calls while collecting the first error which occurred, and so forth. There are almost certainly even some libraries you can use which provide this utility class out of the box.

When using this pattern, I have noticed there seems to be a temptation to avoid using actual exception classes for the failure object, but if you're working inside a language where thrown exceptions are a feature, I recommend still using the provided class hierarchy which is otherwise used for thrown exceptions. My reasoning:

* The caller might want to rethrow it as an actual exception, even if _you_ didn't want to
* The caller might want to attach your failure object as the cause for their own exception
* The caller might want to log a stack trace of the failure—by returning a simpler object, you throw away all the useful diagnostics about where the error occurred

Other than that, this is a fairly straightforward pattern to use, and people familiar with `Optional<T>` will already get the gist of how to use `Try<R>`.

Advice for this one:
* Only use it in those cases where the error _must_ be handled by the caller. Because it essentially makes the exception a checked exception, you want to try to avoid forcing people to handle every possible kind of error. This pattern is a bad fit for _unexpected_ errors because it increases the burden on the caller using the API.
* When possible, use real exception objects to represent the error conditions, so that the caller can treat them the usual way if they want.

# Tying things up

The tl;dr comes down to:

* If you're using a functional style, go with `Try<T>` monads.
* If you're using an object-oriented style, choose between `Try<T>` monads or thrown exceptions and stick to it consistently, and if you do use thrown exceptions, beware of which ones you make checked exceptions.
* If you have no better option, return 0 for success and a unique error code for failure, and provide a function to explain the error code.
* Regardless of which you choose, consider what the caller is going to want to do when they get the error.