Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct design for Haskell exception handling

I'm currently trying to wrap my mind around the correct way to use exceptions in Haskell. How exceptions work is straight-forward enough; I'm trying to get a clear picture of the correct way to interpret them.

The basic position is that, in a well-designed application, exceptions shouldn't escape to the top-level. Any exception that does is clearly one which the designer did not anticipate - i.e., a program bug (e.g., divide by zero), rather than an unusual run-time occurrence (e.g., file not found).

To that end, I wrote a simple top-level exception handler that catches all exceptions and prints a message to stderr saying "this is a bug" (before rethrowing the exception to terminate the program).

However, suppose the user presses Ctrl+C. This causes an exception to be thrown. Clearly this is not any kind of program bug. However, failing to anticipate and react to a user abort such as this could be considered a bug. So perhaps the program should catch this and handle it appropriately, doing any necessary cleanup before exiting.

The thing is, though... The code that handles this is going to catch the exception, release any resources or whatever, and then rethrow the exception! So if the exception makes it to the top-level, that doesn't necessarily mean it was unhandled. It just means we wanted to exit quickly.

So, my question: Should exceptions be used for flow-control in this manner? Should every function that explicitly catches UserInterrupt use explicit flow-control constructs to exit manually rather than rethrow the exception? (But then how does the caller know to also exit?) Is it OK for UserInterrupt to reach the top-level? But in that case, is it OK for ThreadKilled too, by the same argument?

In short, should the interrupt handler make a special case for UserInterrupt (and possibly ThreadKilled)? What about a HeapOverflow or StackOverflow? Is that a bug? Or is that "circumstance beyond the program's control"?

like image 544
MathematicalOrchid Avatar asked Jan 26 '13 15:01

MathematicalOrchid


People also ask

Does Haskell have exception handling?

The Haskell runtime system allows any IO action to throw runtime exceptions. Many common library functions throw runtime exceptions. There is no indication at the type level if something throws an exception. You should assume that, unless explicitly documented otherwise, all actions may throw an exception.

How do you use try in Haskell?

try :: Exception e => IO a -> IO (Either e a) try takes an IO action to run, and returns an Either . If the computation succeeded, the result is given wrapped in a Right constructor. (Think right as opposed to wrong). If the action threw an exception of the specified type, it is returned in a Left constructor.


2 Answers

Cleaning up in the presence of exceptions

However, failing to anticipate and react to a user abort such as this could be considered a bug. So perhaps the program should catch this and handle it appropriately, doing any necessary cleanup before exiting.

In some sense you are right — the programmer should anticipate exceptions. But not by catching them. Instead, you should use exception-safe functions, such as bracket. For example:

import Control.Exception

data Resource

acquireResource :: IO Resource
releaseResource :: Resource -> IO ()

workWithResource = bracket acquireResource releaseResource $ \resource -> ...

This way the resources will be cleaned up regardless of whether the program will be aborted by Ctrl+C.

Should exceptions reach top level?

Now, I'd like to address another statement of yours:

The basic position is that, in a well-designed application, exceptions shouldn't escape to the top-level.

I would argue that, in a well-designed application, exceptions are a perfectly fine way to abort. If there are any problems with this, then you're doing something wrong (e.g. want to execute a cleanup action at the end of main — but that should be done in bracket!).

Here's what I often do in my programs:

  1. Define a data type that represents any possible error — anything that might go wrong. Some of them often wrap other exceptions.

    data ProgramError
      = InputFileNotFound FilePath IOException
      | ParseError FilePath String
      | ...
    
  2. Define how to print errors in a user-friendly way:

    instance Show ProgramError where
      show (InputFileNotFound path e) = printf "File '%s' could not be read: %s" path (show e)
      ...
    
  3. Declare the type as an exception:

    instance Exception ProgramError
    
  4. Throw these exceptions in the program whenever I feel like it.

Should I catch exceptions?

Exceptions that you anticipate must be caught and wrapped (e.g. in InputFileNotFound) to give them more context. What about the exceptions that you don't anticipate?

I can see some value in printing "it's a bug" to the users, so that they report the problem back to you. If you do this, you should anticipate UserInterrupt — it's not a bug, as you say. How you should treat ThreadKilled depends on your application — literally, whether you anticipate it!

This, however, is orthogonal to the "good design" and depends more on what kind of users you're targeting, what you expect of them and what they expect of your program. The response may range from just printing the exception to a dialog that says "we're very sorry, would you like to submit a report to the developers?".

like image 183
Roman Cheplyaka Avatar answered Oct 21 '22 07:10

Roman Cheplyaka


Should exceptions be used for flow-control in this manner?

Yes. I highly recommend you read Breaking from a loop, which shows how Either and EitherT at their core at nothing more than abstractions for exiting from a code block early. Exceptions are just a special case of this behavior where you exit because of an error, but there is no reason why that should be the only case in which you exit prematurely.

like image 29
Gabriella Gonzalez Avatar answered Oct 21 '22 07:10

Gabriella Gonzalez