Exceptions and return values
To signal the impossibility of a function to perform its assigned task, two common representations are used: using the return value of the function to signal the failure (using either a variant return type or an output argument to return the actual result of the function if there is one, or throwing an exception.
Return values have several shortcomings that exceptions solve.
- Return values can be ignored, meaning that the main program flow can continue executing as if a failure did not happen. This can lead to the program silently ignoring the failure of a necessary operation (for instance, noticing that a file could not be saved before quitting, leading to data loss) or to the manipulation of invalid data (for instance, using data that could not be loaded, which creates erroneous results, a hard crash, or a security vulnerability). By using exceptions, the execution flow will be interrupted even if the programmer forgets to check a return value.
And automating the resolution of “the programmer forgets” issues is a good thing.
- In many languages, exceptions are the only means of signaling failure in certain language constructs. This includes constructors and operators. Of course, there is another way of signaling failure in a constructor, by using a “failure” state for the object. However, unless the intended semantics of the object include that failure state (as would be the case, for instance, for a read-from-file stream which may encounter many errors beyond the file-not-found failure in the constructor) introducing that state forces all users of the object to test for that failure state before using the object. This is usually not acceptable for common objects, and often leads to the users forgetting or outright skipping the test.
Of course, in a program where failure recovery is not important, or which is small enough to warrant good programming practices from beginning to end, return values can be an acceptable choice. If a program is not used to manipulate critical data (for instance, a video game) and is properly sandboxed (no write access to disk except for select locations), then gracefully exiting with an exception or crashing because of data corruption following an untested return value are more or less equivalent (even if the latter feels somewhat less professional).
A common argument for and against exceptions is the syntactic weight of exception handling. In this regard, having to place a try { … } catch() { … } around every statement that might throw an exception is indeed a lot of overhead.
Did he just say that?
Yes, I did. You might have to place a try-catch block around every statement that might throw. Of course, this is somewhat alleviated by the fact that you can incorporate several statements in the same try-catch block (assuming that they don’t throw the same exceptions or that you don’t need to treat them differently), with the obvious limit that there are not that many statements that can throw in a given function anyway.
But that’s not the real issue with the statement, is it? The typical philosophy of the “good” user of exception is that exceptions should be left to propagate up the call stack through one or more functions until something can handle them. This is a very bad idea.
Exceptions are a means for a function to communicate with its environment. If you look at exceptions with the eyes of a functional programmer, you’ll notice that throwing an exception is nothing more than returning a variant type which can be either a value or an error, with the language support for the semantic sugar that says expressions or statements (other than exception handlers) that involve an exception merely evaluate to that exception themselves. As such, and as often observed by designers, thrown exceptions are part of a function’s signature as much as its arguments or its return value. Java went as far as adding checked exceptions (listing and type-checking exceptions thrown by a function as part of that function’s signature), an attempt which can be praised, though it fell short because of the lack of expressiveness of the associated type system.
The consequence is that if a function is somehow part of the public interface of a class or module, its signature must match its documented semantics. In the same way that one does not expose internal implementation details through a function’s arguments or return type, one should not expose those same details through the exceptions thrown by that function. If you look for a second at the exceptions thrown by the implementation of the function, they are either specific or generic.
A specific exception reveals explicit details about where it was thrown as why, and incorporates that information in its type (so that the appropriate handlers may intercept it). Including those exceptions in the signature of a function would reveal its internal implementation: where one would call a function to get the data received through a socket for a certain user, one would expect an “Unknown user identifier” exception, and not a “Key not found in dictionary” or “Index out of bounds in array” exception. What is relevant here is not the implementation information (a dictionary or array is used, and a key or index was invalid) but the semantic information that such an implementation cannot explain (that the required user was not found). When such exceptions are thrown, it is imperative that they are caught and translated into new exceptions before they leave the function. Of course, this imposes absolutely no constraint on private functions, which may let escape any implementation-related exceptions they wish.
A generic exception reveals no useful information to the type system (and thus does not allow efficient error recovery beyond a “Retry, Abort, Ignore” prompt) and often does not reveal any information to the user either. Consider the typical exception of the form “I failed because {string message describing cause}” or “I received an invalid argument {string message describing cause}” and its typical consequence : a 30-line Java call stack with an exception at the top that cannot be comprehended by the user without access to the code that threw it.
The inheritance-based solution of inheriting a specific exception from a generic one works in that it doesn’t reveal information about the internal implementation, but it does not improve the information given about the failure in terms of the public interface.
The end result is that most public functions will either have to catch-and-throw every exception (translating them along the way) or give up and provide overly generic useless explanations.
Polymorphism
A notable exception is when the function is not aware of the exceptions that a function it calls may throw. The situation where this happens is when (through object or functional polymorphism) the function being called was provided to the function doing the call through means other than compile-time specification (for instance, as an argument, or as a member variable of the object containing the function doing the call).
This is correct behavior: somewhere up the call stack, there is a function which is aware that the caller function was connected to the called function (because it, or a function of the same module, did that connection in the first place), and is thus aware that the called function may throw a certain set of exceptions, which it may handle.
A classic example would be a typical for-each function which calls another function on every object in a collection. The for-each function cannot know, in advance, that the other function opens a file and may throw a “File not found” exception as part of this execution, and thus cannot and should not translate that exception. However, the caller of the for-each function provided it with the function that it knows will attempt to open a file. As such, it can use an exception handler for the “File not found” exception that escaped the for-each function.
Two issues appear here. The first is the fact that most type systems which include exceptions (such as Java’s checked exceptions) cannot express the behavior “I may throw anything my argument throws, plus these additional exceptions of my own” : they need explicit lists, and while inheritance does alleviate this burden, it results in the information loss related to the lack of constraints between types present in the signature.
The second issue is that the intermediary function may contain handlers of its own (because it might do work other than just calling the function in special ways). These handlers may catch the exceptions thrown by the polymorphic function or object, which will confuse both the intermediary function (it expects issues for its own code, not for the called code) and the original function (which expected a failure). Again, through proper application of an exception-handling type system, a function may only catch exceptions which it knows can be thrown, bringing the exception-handling solution closer, in terms of functionality, to using continuations for exception handling.
Hi. I'm Victor Nicollet,
Recent Comments