Error Checking and Exceptions

Posted on November 28, 2016
by Adam Wespiser

Illusions of control are common even in purely chance situations. They are particularly likely to occur in setting that are characterized by personal involvement, familiarity, foreknowledge of the desired outcome, and a focus on success.Suzanne C. Thompson

Error Exceptions and All That!

When the user enters incorrect input, we can say they have made an error, and people understand this statement as a general term. However, we are in the business of engineering, and there is a technical distinction between errors and exceptions. Errors, refer to situations where our project cannot handle itself, and we must change the source code to remedy the situation. These are unexpected situations. Exceptions, on the other hand, represent expected, but still irregular situations that we can control. Exceptions can represent problems with a potential Scheme program, like an error parsing, or a bad special form. Haskell Wiki.
The sources for errors and exceptions in Haskell are as follows:Exceptions: Prelude.catch, Control.Exception.catch, Control.Exception.try, IOError, Control.Monad.ErrorErrors: error, assert, Control.Exception.catch, Debug.Trace.trace. error is syntactical sugar for undefined.
Try to keep these in mind, but expect to see only exceptions, unless I’ve made an error in my assertion, in which case I’ll have to trace through and catch my mistake.
We say an exception is checked when it after it is “thrown”, another part of code “handles” or “catches” it. This is how our exception system in Haskell will work.

Exceptions Everywhere!

Undefined, unexpected, and generally out of control situations are present in any kind of large system, especially one interacting with the outside world or dealing with use input as complex and complicated as a programming language. Control is an illusion. For our system, we must accept user input, determine that it is valid Scheme syntax, then compute that abstract syntax into a final value. During this process we may interact with the file system or network. It is especially important for programming languages to report and describe the nature of the irregularity.
Thus, there are three types of exceptions that exist in our implementations of Scheme: Parsing, Evaluation, and IO. Each of these originate in a distinct type of activity the parser or interpreter is undergoing, but all of them are end up going through the Eval monad and are caught and displayed in the same place. (see Eval.hs)

Above we have the code to catch an exception. We use Control.Exception.try, then subsequently fromException and case to take an exception and unwrap it into a LispException. For running programs, we might not need to catch an exception thrown in the code, displaying the exception is enough for the user to fix the problem. However, for the REPL, it would be a major pain if we required our users to restart the REPL every time they made a mistake while prototyping a new idea.

Defining an Exception

For our Scheme, an exception will be defined for a internal misuse of a function, a user deviation from accepted syntax or semantics, or the request of an unavailable external resource. Exceptions are thrown in the monad transformer stack, Eval, and caught with the safeExec function, which is convenient for us, because we can throw exceptions from any function return Eval LispVal, which is most of our evaluation code!

Each of these data constructors distinguish the source of their error. Whenever a LispException is created, it is then immediately thrown. Ideally, they provide useful information to the user on how to debug their program after a LispException is returned. Though not as descriptive as a fully featured language, we do have the capacity to provide a fair amount of information, including the a custom message and LispVal that are not compatible. The key to improvement here is keeping track and passing more information to LispException when it is created then thrown.

Similar to our showVal, from Chapter 1, we override the show Typeclass to give a custom message. The showError has a special case for PError, which uses String and just wraps the error message from the parser. The next source IO, can also be tricky. Although we have the ability to throw an IOError, if there is an unchecked exception during IO operations, it will fall through and not be handling via our LispException pathway.

Conclusion

Accidents will happen, and so we have exception handling for IO, parsing, and evaluation exceptions via our LispException type and our handy Eval monad transformer stack. Exceptions are realized everywhere we have functions that evaluate Scheme code, so handling them is composed into our Eval monad. Verbose exception messages are vital to usability, and we must report enough information to pinpoint the user’s misuse of proper syntax, semantics, or resource request. Our main liability with the current monad transformer stack, is that IO can throw an error that is not caught, an unchecked exception. However, we are only using IO to read files, not maintaining open connections for long periods of time, or dispatching concurrent operations on shared resources, so our exposure is minimal. If this is a major concern for you, read the section on “Alternative Exceptions”, which discusses other ways to handle exceptions in Haskell.

[ Understanding Check ]

Go through Eval.hs, find an LispException used in a few places and replace it with a new error that is more specific, or merge two LispException that are better served by a single constructor. Include support for showError.
Many programming languages have information like, “line 4, column 10: variable x is not bound”. How would you go about adding line and column information to messages passed to throwError in Eval.hs?
We are taking a basic approach to format error messages: T.concat and show. Text.PrettyPrint offers a rich way to display messages using pretty print combinators. Implement a PrettyPrint interface for LispException that provides a uniform interface.

Alternative Exceptions (skippable)

We are handling errors in a very basic way. The use of IO causes some trickiness that we won’t be able to handle. Here’s why:

async exceptions We entirely avoid this topic, but they cannot be ignored for most industrial applications. Control.Concurrent.Async defines the library, which makes it a little bit more difficult to run a few threads, then ignore an exception thrown in a thread while other threads exit successfully. Exceptions in asynchronous threads are known as asynchronous exceptions.

ExceptT someErrorType IO a considered bad Exceptions Best Practices. The authors list three reasons why this is considered an anti pattern: 1) Its noncomposable, we see this when we have to do add in the Parser error, and the information in the parser error is not completely congruent with the information we pass to other error messages. 2) It gives the implication that only LispException can be thrown. This is true, during slurp or any IO operation, an error can be thrown that will not be caught. 3) We haven’t limited the possible exceptions, we’ve just added throwError or liftIO . throwIO.

The list is pretty daunting, and building upon the approach taken here is a good path forward. If conceptual simplicity is highly valued, biting the bullet and just using the anti-pattern ExceptT (LispException IO) a might not hurt too bad. Its not super great code, but its pretty simple to understand, and forces the either error value logic to happen during monadic evaluation. IO really seems to be tricky here, and if we were running multiple threads, we ought to be handling async exceptions. There’s no right answer, but a consensus has formed around the opinions exposed in Exceptions Best Practices.