Is there an agree-on best practice for aggregating and handling typed errors across many layers of functions in a larger Haskell application?
From introductory texts and the Haskell Wiki, I take that pure functions should be total - that is, evaluate to errors as part of their co-domain. Runtime exceptions cannot be completely avoided, but should be confined to IO and asynchronous computations.
How do I structure error handling in pure, synchronous functions? The standard advice is to use Either as return type, and then define an algebraic data type (ADT) for the errors a function might result in. For example:
data OrderError
= NoLineItems
| DeliveryInPast
| DeliveryMethodUnavailable
mkOrder :: OrderDate -> Customer -> [lineIntem] -> DeliveryInfo -> Either OrderError Order
However, once I try to compose multiple error-producing functions together, each with their own error type, how do I compose the error types? I would like to aggregate all errors up to the UI layer of the application, where the errors are interpreted, potentially mapped to locale-specific error messages, and then presented to the user in a uniform way. Of course, this error presentation should not interfere with the functions in the domain ring of the application, which should be pure business logic.
I don't want to define an uber-type - one large ADT that contains all possible errors in the application; because that would mean (a) that all domain-level code would need to depend on this type, which destroys all modularity, and (b) this would create error types that are too large for any given function.
Alternatively, I could define a new error type in each combining function, and then map the individual errors to the combined error type: Say funA comes with error-ADT ErrorA, and funB with ErrorB. If funC, with error type ErrorC, applies both funA and funB, funC needs to map all error-cases from ErrorA and ErrorB to new cases that are all part of ErrorC. This seems to be a lot of boilerplate.
A third option could be that funC wraps the errors from funA and funB:
data ErrorC
= SomeErrorOfFunC
| ErrorsFromFunB ErrorB
| ErrorsFromFunA ErrorA
In this way, mapping gets easier, but the error handling in the UI-ring needs to know about the exact nesting of functions in the inner rings of the application. If I refactor the domain ring, I do need to touch the error unwrapping function in the UI.
I did find a similar question, but the answer using Control.Monad.Exception seems to suggest runtime exceptions rather than error return types. A detailed treatment of the problem seems to be this one by Matt Parson. Yet, the solution involves several GHC extensions, type-level programming and lenses, which is quite a lot of stuff to digest for a newbie like me, who simply wants to write a decent application with proper "by the book" error handling using Haskell's expressive type system.
I heard that PureScript's extensible record would allow to combine error enums more easily. But in Haskell? Is there a straightforward best practice? If so, where can I find some documentation or tutorial on how to do it?