Error handling in modern languages

This is a sequel to an earlier post Why I prefer error values over exceptions.

In recent languages one of the more prominent differences as opposed to the more well established languages is the way error handling is performed. Let’s have a look at various mechanisms of error handling as they are available in established languages and new mechanisms that are provided in more recent languages.

Ye olde ways

First we look at a number of well established / “old style” error handling techniques.

Error handling in recent languages

More recently developed languages provide different mechanisms for handling errors. We’re going to look at error handling in the languages Rust, Go and …

Error handling patterns

With new mechanisms for error handling come new patterns for the actual handling of errors. As expected, these patterns depend on the underlying mechanisms. We will look at how errors are handled in Go and Rust.

Go error handling tactics

In Go, one handles errors by verifying the returned error instance. In case of nil, no error occurred and we can continue logic with the acquired execution result. However, in case of repeated method calls this would mean that we need to verify the error value return on every call. This can be quite bothersome, but at the same time this is its strength, because you are sure that you handle every error appropriately.

It will depend on the semantics of the error value, whether or not the error is of immediately use or needs to be returned to a higher level. There are a number of different tactics that can be applied where we treat error value returns slightly different.

  1. Immediately check error value and act appropriately, such as halting further execution of the method and immediately returning an error value. This is the most obvious one.

  2. First process the (partial) result value that is acquired. Then check the error value and if non-nil stop processing.
    This use case is common with IO operations. For example, a read operation may get interrupted because of a lost connection, however it will still return as much of the data as it was able to acquire. The error is still significant, of course. After processing the remaining data, we continue to handle the error.

  3. Keep executing but preserve the first/last error value that is acquired and non-nil. This is useful in cases where it does not make sense to stop half-way through the processing cycle. We return the first/last acquired error value after finishing processing.

  4. Process everything and gather all non-nil errors in a list. At the end of processing, a list of error values is returned for the caller to use/report.

Repeated behavior can be simplified by creating a closure function at the start of execution of such a method. Instead of repeating the same code at every function call, we can wrap the function call inside the closure and let it handle the error values appropriately, whatever your error handling tactic might be.

Go: verifying that all errors are handled

In Go, it is possible to simply not assign the error result to any variable. The error is implicitly silenced. To discover accidentally silenced errors, we can run errcheck. errcheck reports on all function calls where the returned built-in error type is not assigned to any variable. Errors that are of no value can be explicitly silenced by assigning them to ‘_’ (underscore). errcheck considers this an explicitly silenced error and does not report on such function calls by default.

Rust’s try! macro

Rust relies quite a bit on macros. Macros are used to simplify things for programmers that can already be resolved at compile-time. Similarly, there is a try! macro. (Macros can be recognized by the ! suffix.) This is a short-hand for verifying the result and returning either the result value of a successful execution, or the error value in case of unsuccessful execution.

This approach is similar to Go’s manual checking, except that the try! macro is a short-hand that can be used as long as the function in which it is executed has a matching signature. It needs to return a Result-type of matching result value type and error type.

For cases where this standard pattern cannot be applied, Rust relies on the programmer to correctly process the returned Result.

References

If you are interested in error handling, safe and predictable programs and systems programming. Be sure to read The Error Model by Joe Duffy. It is about a programming language called Midori. In this post, he discusses error handling in great detail. He discusses the various models that are known and all their advantages and disadvantages, both from the perspective of effectivity of use and the efficiency of execution.


This post is part of the Error Handling series.
Other posts in this series: