« Back to Blog

Modern Programming Languages: How to Handle Errors

By Ryan Veazey
Apr 16, 2018

Virtually every program will, at some point, encounter some sort of unexpected state. This can range from things you can probably avoid, such as trying to dereference a null pointer, to things outside the control of a developer, such as the system running out of disk space or a network connection being unavailable.

Different patterns emerged to communicate and handle these situations, many of which have been incorporated as core features of modern programming languages.

Status Codes

One of the oldest ways of handling errors you’re likely to still encounter involves the concept of predefined error values or status codes. For example, a function which returns the number of files in a directory may return -1 (or some other negative value) if the directory doesn’t exist. It will then be expected that the caller of the function will check for this condition rather than simply passing along the value as returned. In some systems, the specific value returned may include more specific information. Perhaps -1 when a directory doesn’t exist, but -2 when the process cannot access the directory. In Unix/Linux, a process returning a value of 0 is considered successful. Any other value is considered some sort of error which may or may not represent more information other than simply that something unspecified went wrong.

There are several potential problems with this style:

  1. It may be difficult to differentiate between error values and valid values.
  2. There is no standard definition for the meanings of error values. The developer is responsible for creating a lookup table, if applicable.
  3. Due to the point above, it can be difficult to know how to appropriately handle an error.

Exceptions

Although Java is probably the language that made the concept of exceptions widely known, most modern languages include them. While there are some differences implementation, the basic pattern of exceptions involves the following steps:

  1. Something unexpected happens.
  2. This is detected, and an instance of an Exception class (or a subclass) is created with information about the specifics of what went wrong.
  3. This exception is thrown, which is essentially a series of immediate escapes back up the call stack until the exception is caught, or handled. If the entire call stack is exhausted without the exception being handled, the program generally terminates.

At its most appropriate usage, an exception is aptly named – the situation is exceptional. Unfortunately, this isn’t always the case. Even within this category, there has been a decent shift in both what is considered an exception and how best to handle them. You can see this shift, for example, inside C#. Libraries that were written early on in the language involve what might be charitably be described as as an exuberance for throwing exceptions.

Retrieving Information

For an example, there is a built-in library for making http (web) requests. A web request can succeed (200 status code) or it can be any number of results which may or may not be truly exceptional. A not found (404) response could, in some ways, be considered an exception. However, many times a developer may want to specifically check for the existence of an endpoint, for example, to see if some URL existed. This describes a situation in which a request was properly formed, sent, and received a response for which there was simply not a resource.

Turning this into a boolean can take some work:

public static bool Is404(string url)
{
   var request = WebRequest.Create(url);
   request.Method = "HEAD";
   try
   {
       request.GetResponse();
   }
   catch (WebException ex) when ((ex.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.NotFound)
   {
       return true;
   }
   return false;
   }
}

Overuse

Take, for example one of the original C# database libraries which map result sets of database queries to C# types. Null is a valid value type for certain columns in tables which represents the lack of a value. However, the designers of the library decided that the nullity of a value was an exceptional case where the act of even retrieving a value which is null from a data set should result in an exception.

Both the problems above are design choices which use exceptions rather than problems inherent to exceptions themselves, but similar situations are common.

Handling too Soon

In some languages, such as Java, there is a requirement to either handle (catch) every possible exception or to mark the containing method as one which throws that type of exception. If the method is marked as throwable, then every method which calls that method needs to make that same decision until it is finally caught. This explicitness can work well as documentation, but it can also make refactoring more cumbersome and encourage handling the exception too soon.

There are many times when you want an exception to bubble up quite a few levels before you really know what you want to do with an exception. Depending on the language, libraries, and logging solutions, trying to handle exceptions immediately when they occur can sometimes mask the real problem. For example, if there was an exception trying to hit a web endpoint, you’d want to see a message that included information about what endpoint and the context of why you were accessing it. That information might be a few steps above the actual request - compare the information presented in an error message such as “HttpException encountered in method PerformGet” vs “HttpException encountered in method FetchUser.”

Error Objects (Go)

Seeing some of the downsides of the overuse of exceptions, the designers of Go decided on a different solution.

Go functions can return multiple values. Any function that can be exception-y, then, returns an error object as one of its values (or its only one for a void function). Any other values returned should be ignored in the case of an error. In this way we can have functions both return data and return errors. Furthermore, as in Java, the fact that a function can return an error is made more explicit.

This seems, at first, like an elegant way to use the language’s other features to avoid the need for something more complicated. Simplicity through omission is a common tool that Go’s designers employed in its creation. Unfortunately, the omission of a language feature doesn’t necessarily mean its users won’t need something to fill that void.

Three common problems present themselves in many non-trivial Go projects:

Lack of Information

Go errors are essentially a string. Your error objects can include more, but the basic error interface does not. This means you’re unlikely to have any information about the current callstack, or if the error was caused by another error (for example a network failure causing a database access failure). It’s up to the designer to cram all the useful information into a string, which can make any sort of a generic handling system complicated. There are libraries and patterns that can add some of this back in, but it is essentially placing a burden on developers that the language could be taking away.

Lack of Stack Unwinding

As mentioned in in the exceptions section, there may be many function calls between where an error originates and where it can be competently handled. Exceptions simply unwind this call stack until they are handled. Errors in Go are simply values. They have no special behavior or syntax of their own and, as such, must be manually checked, augmented, and copied by the developer. This results in a lot of intermediate code that calls a function, checks for any errors returned and, if found, passes those as its own error

Lack of Nesting

A common pattern in exception handling is to nest exceptions to show that one caused the other, like the aforementioned network/database issue. While this must be done somewhat manually, it is a pattern that is built in. Go again puts the burden on the developer of inventing their own pattern or doing without this feature.

Optionals

Many functional languages (and many with influence from functional languages) have the concept of an optional value. This can be though of as a wrapper that is either “no value” or “some value”. An optional integer can be either an integer or no value at all. The optional pattern is used in some languages for at least some types of errors.

One of the biggest benefits of optionals is that once they are unwrapped they are ensured to be a valid value. The compiler treats int and optional int as two separate types, which again makes explicitly clear which code might have problems. This gets around the issue with status codes of confusion between valid values and error values, and many languages have common and clear patterns for pattern matching or branching logic on these sorts of data types. It also creates a compiler-enforced way to avoid most types of null pointer exceptions.

Conceptually, it can work well for situations when you don’t care why something went wrong, only that it did. However, this pattern has the same drawbacks as the Error type pattern in Go - lack of information, nesting, and unwinding.

Hybrid

Many language have slight variations on at least one of these pattens, but an interesting language in this realm is Swift, which has changed quite a bit over its short lifespan.

Swift was initially created without exceptions and depended primarily on optionals for error handling. This made some error handling elegant, but a lot of it cumbersome.

The next version of swift decided to add in exceptions. Optionals were still present as a language feature, but most errors now threw exceptions. With exceptions came most of the benefits and problems of exceptions. Errors became more expressive and handling more explicit, but it made code which didn’t care why a function failed more complicated.

The current version of swift retains exceptions (called errors but using the exception pattern), but allows them to be handled in an uncommon way. Cases where the cause of an error are not important can be greatly simplified by changing an expression which might throw an error to an optional.

This, along with some other features such as the optional-chaining operator (?) and the nil-coalescing operator (??) leads to some really clear, concise ways of handling problems:

var name = (try? getCurrentUser())?.name ?? "Guest"

Above we’re calling a function to get the current user which, for whatever reason, might fail. In this case, we know that this can only fail if the user doesn’t exist or they aren’t logged in - in either case, we simply want to call the user “Guest.”