Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell approaches to error handling

No argument here that there are a variety of mechanisms in place in Haskell to handle errors and properly handle them. Error monad, Either, Maybe, exceptions, etc.

So why is it that it feels much more straightforward writing exception-prone code in other languages than in Haskell?

Let's say I'd like to write a command line tool that processes files passed on the command line. I'd like to:

  • Verify filenames are provided
  • Verify files are available and readable
  • Verify files have valid headers
  • Create output folder and verify output files will be writable
  • Process files, erroring on parsing errors, invariant errors, etc.
  • Output files, erroring on write error, disk full, etc.

So a pretty straight file processing tool.

In Haskell, I'd be wrapping this code in some combination of monads, using Maybe's and Either's and translating and propagating errors as necessary. In the end, it all gets to an IO monad where I am able to output the status to the user.

In another language, I simply throw an exception and catch in the appropriate place. Straightforward. I don't spend much time in cognitive limbo trying to unravel what combination of mechanisms I need.

Am I simply approaching this wrong or is this there some substance to this feeling?

Edit: Okay, I'm getting feedback telling me that it just feels harder but actually isn't. So here is one pain point. In Haskell, I'm dealing with stacks of monads, and if I have to handle errors, I'm adding another layer to this monad stack. I don't know how many lift's and and other syntactic litter I've had to add just to make the code compile but adds zero semantic meaning. No one feels this adds to the complexity?

like image 700
Muin Avatar asked Jun 30 '11 16:06

Muin


People also ask

How do you handle errors in Haskell?

On the one hand, an error is a programming mistake such as a division by zero, the head of an empty list, or a negative index. If we identify an error, we remove it. Thus, we don't handle errors, we simply fix them. In Haskell, we have error and undefined to cause such errors and terminate execution of the program.

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 handle errors in Deconstructors?

Explanation: It will not throw an exception from the destructor but it will the process by using terminate() function.

Is either a Monad Haskell?

Strictly speaking, Either cannot be a monad, as it has kind Type -> Type -> Type ; Either String can be (and is), because it has kind Type -> Type .


2 Answers

In Haskell, I'd be wrapping this code in some combination of monads, using Maybe's and Either's and translating and propagating errors as necessary. In the end, it all gets to an IO monad where I am able to output the status to the user.

In another language, I simply throw an exception and catch in the appropriate place. Straightforward. I don't spend much time in cognitive limbo trying to unravel what combination of mechanisms I need.

I wouldn't say you're necessarily approaching it wrong. Rather, your mistake is in thinking that these two scenarios are different; they're not.

To "simply throw and catch" is equivalent to imposing upon your entire program the exact same conceptual structure as some combination of Haskell's error-handling methods. The exact combination depends on the error-handling systems of the language you're comparing it to, which points to why Haskell seems more complicated: It lets you mix and match error handling structures based on need, rather than giving you an implicit, one-size-fits-most solution.

So, if you need a particular style of error handling, you use it; and you use it for only the code that needs it. Code that doesn't need it--due to neither generating nor handling the relevant sorts of errors--is marked as such, meaning you can use that code without worrying about that sort of error being created.


On the subject of syntactic clumsiness, that's an awkward subject. In theory, it should be painless, but:

  • Haskell has been a research-driven language for a while, and in its early days many things were still in flux and useful idioms hadn't been popularized yet, so old code floating around is likely to be a poor role model
  • Some libraries are not as flexible as they could be in how errors are handled, either due to fossilization of old code as above, or just lack of polish
  • I'm not aware of any guides on how to best structure new code for error handling, so newcomers are left to their own devices

I'd guess that chances are you're "doing it wrong" somehow, and could avoid most of that syntactic clutter, but that it's probably not reasonable to expect you (or any average Haskell programmer) to find the best approaches on their own.

As far as monad transformer stacks go, I think the standard approach is to newtype the entire stack for your application, derive or implement instances for the relevant type classes (e.g., MonadError), then use the type class's functions which won't generally need lifting. Monadic functions you write for the core of your application should all use the newtyped stack, so won't need lifting, either. About the only low-semantic-meaning thing you can't avoid is liftIO, I think.

Dealing with large stacks of transformers can be an actual headache, but only when there's a lot of nested layers of different transformers (pile up alternating layers of StateT and ErrorT with a ContT tossed in the middle, then just try to tell me what your code will actually do). This is rarely what you actually want, though.


Edit: As a minor addendum, I want to bring attention to more general point that occurred to me while writing a couple comments.

As I remarked and @sclv demonstrated nicely, correct error-handling really is that complicated. All you can do is shuffle that complexity around, not eliminate it, because no matter what you're performing multiple operations that can produce errors independently and your program needs to handle every possible combination somehow, even if that "handling" is to simply fall over and die.

That said, Haskell really does differ intrinsically from most languages in one regard: Generally, error-handling is both explicit and first-class, meaning that everything is out in the open and can be manipulated freely. The flip side of this is a loss of implicit error-handling, meaning that even if all you want is to print an error message and die, you have to do so explicitly. So actually doing error-handling is easier in Haskell, because of first-class abstractions for it, but ignoring errors is harder. However, that sort of "all hands abandon ship" error non-handling is almost never correct in any sort of real-world, production use, which is why it seems like awkwardness gets brushed aside.

So, while it's true that things are more complicated at first when you need to deal with errors explicitly, the important thing is to remember that that's all there is to it. Once you learn how to use the proper error-handling abstractions, the complexity pretty much hits a plateau and doesn't really get significantly harder as a program expands; and the more you use those abstractions the more natural they become.

like image 167
C. A. McCann Avatar answered Sep 22 '22 18:09

C. A. McCann


Let's look at some of what you want to do:

Verify filenames are provided

And if they aren't? Just quit, right?

Verify files are available and readable

And if some aren't? Process the remaining ones, throw an exception when you hit a bad one, warn on the bad ones and handle the good ones? Quit before doing anything?

Verify files have valid headers

And if they don't? Same issue -- skip the bad ones, abort early, warn on the bad ones, etc...

Process files, erroring on parsing errors, invariant errors, etc.

Again, and do what, skip the bad lines, skip the bad files, abort, abort and rollback, print warnings, print configurable levels of warnings?

The point is that there are choices and options available. To do what you want in a way that mirrors the imperative way, you don't need any maybes or eithers of monad stacks at all. All you need is throwing and catching exceptions in IO.

And if you want to not use exceptions all over, and get a degree of control, you can still do it without monad stacks. For example, if you want to process the files you can and get results, and return errors on the files you can't, then Eithers work great -- just write a function of FilePath -> IO (Either String Result). Then mapM that over your list of input files. Then partitionEithers the resultant list, and then mapM a function of Result -> IO (Maybe String) over the results, and catMaybe the error strings. Now you can mapM print <$> (inputErrors ++ outputErrors) to display all the errors that came up in both phases.

Or, you know, you can do something else too. In any case, using Maybe and Either in monad stacks has its place. But for the typical error handling cases, its more convenient to deal with them directly and explicitly, and very powerful too. It just takes some getting used to the large variety of functions to make their manipulation convenient.

like image 31
sclv Avatar answered Sep 22 '22 18:09

sclv