Why I prefer error values over exceptions
Wed, Feb 10, 2016 ❝An opinion about errors and exceptions.❞Contents
Also be sure to check out Error handling in modern languages.
Errors vs. exceptions is a topic that seems to pop up whenever one looks at or reads about (typically newer) languages that do not have a notion of exceptions. As with many things in computer science there are often counter parts, contradictions, solutions taking opposite approaches. Errors and exceptions are exactly such counterparts.
In this article, I describe why I prefer error values over exceptions. Now, you have to understand that this preference is bound by the philosophy of the programming language that is used. It makes no sense to go against a whole language and standard library in trying to enforce a error handling style that is not supported by the rest of the community. This preference is only valid as far as the programming languages supports and enables it. There is even less value in having an uncomfortably mixed bunch of error handling techniques. You will not know what to expect and when to expect it, which makes both reading and writing such code far more of a problem.
Nature
First I want to look at the over-all concept of errors, i.e. error values that are returned by a method. Errors are the signals for an individual case of something out of the ordinary. The error is returned as a way of signaling that something was different than expected in one particular case, one particular function call. An error gets returned and can be handled immediately, or stored, like any other value. It is treated like any other value. You can ignore it like any other value, if you like. If you expected a special case, then you can check if expectations match and act accordingly. If you don’t really care about the nature of the outcome, you can ignore the error value completely. In any case, we are talking about receiving a value that may signal an error for an individual operation that may return this error.
Characteristics of errors:
- Just another value. The difference is in the semantics. This value explains how the execution was different from normal expectations, i.e. after the fact.
- Errors only traverse one “level”. The value traverses from called function to calling function.
- Errors traverse using the normal exit points of a function.
- Errors are not propagated unless done so manually by the programmer.
- Errors cannot be returned unless prepared for in the function. The function definition needs to include a type for the error. Thus the possibility of an error is made explicit.
- An error value is limited to the information that is explicitly provided. Typically a value that hints at the nature of the error.
- At the point of handling, exact context information is still available. Consequently, the error can be handled with an exact context. (Note that the point of handling is the calling method, so this trivially explains why context information is available and accurate.)
Note that because of this, the error itself need not provide context information inside the error message. If necessary, this can be done in the calling logic. The error itself contains only those details that explain what determined the error condition in the current execution logic.
Exceptions, on the other hand, are a generalized mechanism for taking the emergency exit when something unexpected happens. This is a fundamentally different tactic. That is why exceptions are propagated and handled differently.
Characteristics of exceptions:
- Special type of event that takes a different route than your normal code.
Exceptions, when thrown, exit a function/method immediately, i.e. at other points than your normal flow of control does. Normal control flow only returns at return-statements (or implicitly at the end of a function). - Exceptions propagate automatically.
- Exceptions traverse many levels up the call stack, unless stopped. If an exception is never caught, it will kill its thread of execution, possibly killing the program with it.
- The emergency exit through which exceptions “escape” always exists, implicitly.
- Exceptions can be thrown at (virtually) any point in the code and are unannounced. Java’s checked exceptions have slightly different properties. We currently discuss the unchecked exceptions only.
- Exceptions contain an error message. Additionally they (typically) also contain relevant information to programmers such as a stack trace containing classes and line numbers.
- Handling: Exceptions are thrown and can be caught at any level, if at all. The order is enforced by the fact that exceptions immediately become effective as they use the “emergency exit” and propagate up the call stack until handled. (If not, the program crashes.) As an exception may traverse multiple levels before being caught, or an exception is caught immediately but in a catch-clause that encompasses a whole block of code, the origin and cause of the exception are (almost) always obscured at time of handling. This interferes with distinguishing specific error cases. (Note that I am referring to exception handling in code, not to manually reading the stack trace after the fact.)
Whichever way you choose, the challenging part is always in the handling of errors/exceptions, rather than in creating them.
Handling
I was wondering what might be the ideal use case for exceptions. Actually, the best use case I can think of is when you only are required to process something correctly if the input is perfect and thus you can process it completely without encountering even a single exceptional case. Unless your input is perfect, you are allowed to crash (“escape”) in the simplest, most straight-forward way possible. Any exception thrown can be considered a programming error, unless the input itself is bad of course, in which case you can be proud of the produced exception. This implies that exceptions should not be caught, as it signals a programming error which must be resolved at the root. This approach matches best with the approach that Erlang takes: You assume that failures happen. Instead of trying (in vain) to prevent or avoid all possible failure cases, focus on quick and “painless” recovery after a failure happens. This is actually perfectly supported by the characteristics of exceptions. (See the listing of characteristics above.) A consequence of this approach is that you would need to adopt quite a defensive style of programming, as you would need to avoid exceptions at all cost.
The ideal way is very “pure”, the rules black and white. As soon as something out of the ordinary happens, you throw an exception and let the program “crash”. That means that you do not catch any exceptions. The problem with “the ideal way” is that it is not the way most applications are expected to work in real life. Often we expect to receive, at the very least, partial results. We expect a report telling us what is wrong in non-technical terms. Sometimes we want to return a valid response containing an error code. We expect recovery from a decent subset of all possible errors at various levels in the application, instead of starting over after each minor mistake. Furthermore, we would like to give an accurate explanation for what actually went wrong, which means that you need identifying information in order to derive the origin and thus the cause of the exception.
When we take into account states, inside and outside of your application, you add a whole new level of difficulty. Application state may cause repeatedly, even infinitely, failing applications if at some point this invalid state is reached. Exceptions would not attempt to resolve this bad state and instead start over, just to trip over the same bad state again. State outside of your application, such as the availability of required systems also complicate the application’s expected behaviour. If a dependency is unavailable for a time, you would likely not want your application to fail completely, even if the request is not perfect given the circumstances.
Given these expectations of reasonable recovery from failures, the preference for not taking on an extremely defensive style of programming, the necessity to continue operating (within reason) in a less than perfect environment and many other reasons, it will be obvious that “the ideal way” for using exceptions will not work.
… and that’s where the lines start to blur …
Consequences
If an exception is not absolute. If an exception sometimes becomes common, even expected, then it is no longer an exception. Instead it becomes part of your normal control flow. Your methods no longer exit solely at return
statements, but sometimes at a throw
statement. Once you are past this taboo and occasionally throw in exceptions as part of the control flow, it becomes easier to throw in exceptions for other purposes. For example, as a micro-optimization: there are tricks/practices that leverage the “emergency exit” behaviour of exceptions as a way of handling the occasional bad values as opposed to always checking the (possibly bad) value beforehand. This is less costly over the long run, as we assume good value and don’t have to pay the cost of verifying values before use. (Note that it is besides the point whether this micro-optimization is good or bad.) There are many other practical applications of exceptions, and that’s fine if it works for you. However, exceptions still have some characteristics that are undesirable for use as ordinary control flow. It is hard to find a decent sized application (that has not had a long stabilization period) that does not occasionally slip up and let an exception slip through. The necessity to switch focus to handling the exception as soon as it pops up. As well as the fact that only 1 exception can ever be in transit at one time. Not to mention that at some point you will end up accidentally catching (or even unintentionally silencing) serious exceptions that should have gotten through in the attempt to appropriately handle irrelevant exceptions. There is no way to distinguish between the two.
To be fair, it is also possible to silence errors with error handling. This tends to happen less often because we evaluate individual method calls. You cannot accidentally silence an error because the method is called inside of an existing “error handling block”, analogues to a try-catch block for exceptions. If an error value is silenced, then we have at some point consciously decided to do so.
Considering all of this, I find exceptions are very useful as a last resort, to detect programming mistakes (exceptionally useful, I would say), but not at all appropriate as your every day error handling mechanism. Every day error handling is more like reactive control flow adjustment than the handling of serious errors signaling imminent failure. Unfortunately, exceptions seem to be default way of handling any kind of error from serious and fatal flaws to minor deviations and consequently both throwing and handling are encountered in all possible varieties.
Update: A very nice, although very technical, article on error handling and exceptions is The Error Model by Joe Duffy. It is amazingly detailed on the good, the bad and the ugly of error handling.
As a side note … there is the notion of a “checked exception” in Java. This is an attempt to provide the mechanism of exceptions without the risk of letting an exception run loose uncontrolled. Checked exceptions force the author to choose between either catching or throwing. The choice must be made explicit in the code. This mechanism works as a way to better control exceptions, however the other disadvantages do still apply. Furthermore, checked exceptions are sometimes avoided and labeled as a failed experiment which is also the reason why some languages do not have a notion of checked exceptions at all. In Java, checked exceptions are still considered to be a significant part of the language.
As a second side note … when talking about error values, this is not restricted to C-style error values. There are obvious flaws in returning sentinel values as the return type that was originally intended for a computational result. Error values are preferably provided through a dedicated type and as its own return value. Thus when no error occurred, this particular return value is null
/unused/not present. Furthermore, through the dedicated error type we avoid misinterpreting a (sentinel) error value as something else. We of course leverage a language’s type system to the fullest to avoid inconvenient/ambiguous circumstances.
References
- Dave Cheney: Andrei Alexandrescu on exceptions
- 250bpm: Why should I have written ZeroMQ in C, not C++ (part I)
- The Rude Programming Blog: Errors and Exceptions
- Rob Pike: Errors are values
- Dave Cheney: Why Go gets exceptions right
- StackOverflow: C++: do you (really) write exception safe code?
- The Java Tutorials: Unchecked Exceptions - The Controversy
- Joe Duffy: The Error Model