Hacker News new | ask | show | jobs
by the_duke 1576 days ago
I only used Haskell for small projects. I admire the language, but I found error handling to be one of the weakest and most inconsistent elements. To the point of being annoying and time consuming.

Several popular libraries I used threw exceptions for expected failures (like a non 2xx HTTP response) and required wrapping.

Even the standard prelude is full of partial functions. (head...).

I saw a wild mix of Either, exceptions and custom monads all over the ecosystem. So if you want to have a coherent strategy you end up doing a lot of error juggling.

Manual errors with Either can make it very hard to figure out where an error came from because they don't capture backtraces. So if you don't have a very specific error for each failure point you are left guessing and debugging.

3 comments

I have to echo your point on inconsistency.

Our company uses Haskell and the Haskell team love to define their own solutions which make things even more inconstant. For error handling they end up using an extensible type-level-list containing possible error types, embedded in an extensible effect monad. We also have list, array, vector, and our own collection types in the same place.

It feels like everyone want to make things better by using/making something new, instead of making them consistent.

> Manual errors with Either can make it very hard to figure out where an error came from because they don't capture backtraces. So if you don't have a very specific error for each failure point you are left guessing and debugging.

Why wouldn't you have a very specific error for each failure point?

Funnily enough, I theoretically agree with your point but can't remember being bitten by it in practice for some reason.

Maybe you can help by giving an idea of a real world example of this?

> they generally don't capture backtraces

Backtraces with higher-order functions, lazyness, partial applications, and all the transformations going on (SKI, CPS, or whatever the GHC does), I don't think any kind of backtrace would be legible.

The transformations usually can and should be implemented in a way that preserves the original call stack information. However, you are right that laziness makes backtraces less useful: they still are correct, but they pop up in completely unexpected moment.

For example, you do something like “let x = f y in return (g x)”, where x is a (lazy) list, and g :: IO [U] -> V for some types U and V. Then somewhere deep into g’s callstack, 123th element is accessed, which forces its computation, which results in exception. You then get an error, and backtrace should naturally come from function f, but in fact it actually happened while executing g, and if g is missing from the trace, a natural intuition from strict languages would suggest that error happened before execution entered g, because f is called before g, which gets its return value.