<div dir="ltr"><div style="font-family:monospace" class="gmail_default">Hello Brian,<br><br>Thank you for your response!<br><br>> Error handling is hard, no matter how you slice<br>> it. (See<br>> <a href="https://joeduffyblog.com/2016/02/07/the-error-model/">https://joeduffyblog.com/2016/02/07/the-error-model/</a><br>> for a mature analysis of all the options.) <br><br>That was a really nice read! And yes, it was definitely comprehensive, no question.<br><br>I especially appreciated the focus on how Java does Checked Exceptions and Unchecked Exceptions, as well as the strengths and weaknesses of both. I had never really done much trying to make or catch Unchecked Exceptions, but seeing how they "poison the water" was insightful. It was also really interesting to see them talk about how they used assert. Makes me wonder what Java would look like if we had precondition/postcondition tools like that. Or if assert had meaning in the method signature.<br><br>> The benefit of using algebraic data types<br>> (e.g., the Either or Try monads) for error<br>> handling is _uniformity_.  Type systems such as<br>> the simply typed lambda calculus (Peirce, Ch9)<br>> say that if we have types T and U, then the<br>> function type `T -> U` is also a type. Such<br>> function types are well behaved, for example<br>> they can be composed:<br>> if f : T -> U and g : U -> V, then g.f : T -> V<br>> <br>> A Java method<br>> <br>>     static int length(String s)<br>> <br>> can be viewed as a function String -> int.  But<br>> what about a Java method<br>> <br>>     static int parseInt(String s) throws NumberFormatException<br>> <br>> ?  This is a _partial function_; for some<br>> inputs, parseInt() does not produce an output,<br>> instead it produces an effect (throwing an<br>> exception.)  Type systems that describe partial<br>> or effectful computations are significantly<br>> more complicated and less well-behaved. <br>> <br>> Modeling a division result as in the following<br>> Haskell data type:<br>> <br>>     data DivResult = Success Double | Failure<br>> <br>> means that arithmetic operations are again total:<br>> <br>>     DivResult divide(double a, double b)<br>> <br>> This has many benefits, in that it becomes<br>> impossible to ignore a failure, and the<br>> operation is a total function from double x<br>> double to DivResult, rather than a partial<br>> function from double x double to double.  One<br>> can represent the success-or-failure result<br>> with a single, first-class value, which means I<br>> can pass the result to other code and let it<br>> distinguish between success and failure; I<br>> don't have to deal with the failure as a<br>> side-effect in the frame or stack extent in<br>> which it was raised.  The common complaints<br>> about "lambdas don't work well with exceptions"<br>> comes from the fact that lambdas want to be<br>> functions, but unless their type (modeled in<br>> Java with functional interfaces) accounts for<br>> the possibility of the exception, we have no<br>> way to tunnel the exception from the lambda<br>> frame to the invoking frame. <br><br>So, this was interesting to read. And they said something similar in the article that you linked.<br><br>On the one hand, that uniformity allows us to cleanly and easily latch/compose things in a happy path sort of way.<br><br>But maybe I am wrong here, but when we are talking about Sealed Types (Algebraic Data Types), I find that most functions attempt to handle "the good values" of the ADT. Obviously, there are things like Stream or Optional that actually do behave the way that this is described as, but how often are we writing something that rich and involved? Usually, we are doing something more akin to DivisionResult. And in those cases, do we really want to hand over DivisionResult as a parameter as opposed to Success? I guess I just don't see much value in passing the "uncertainty" over to the next level unless that uncertainty is the desirable part (again, Stream and Optional).<br><br>My reasoning for this is from an article by Alexis King (Lexi Lambda) -- "Parse, don't validate" (<a href="https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/">https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/</a>). For me, ADT's are at their best when you can extract the good from the bad, then pass along the safer, more strongly typed good, and then handle the safer, more strongly typed bad as desired. The idea of continuing to pass along the "Unknown" type seems to me like passing along the uncertainty when I could have certainty - whether good or bad.<br><br>> It is indeed true that exceptions carry more<br>> information, and that information comes at a<br>> cost -- both a runtime cost (exceptions are<br>> expensive to create and have significant memory<br>> cost) and a user-model cost (exceptions are<br>> constraining to deal with, and often we just<br>> throw up our hands, log them, and move on.) <br>> On the other hand, algebraic data types have<br>> their own costs -- wrapping result<br>> success/failure in a monadic carrier intrudes<br>> on API types and on code that consumes results.<br><br>Ok, cool. So it's a trade off between performance and information.<br><br>I guess that leads to the next question then -- when does it make sense to include that level of information? Knowing that this is the primary difference sends me down some interesting and probably incorrect trains of thought. If the difference is purely a contextual one, then unless I want context, I should default to an ADT. And it is only when I want context that I should opt into Exceptions. How do you feel about that?<br><br>But that gets even more nuanced because then I am missing out on that chaining map-reduce style flow state that ADT's get. Exceptions don't have that. It really feels like I am comparing apples and oranges, and it's not really a matter of which is better, as opposed to if my context happens to want apples vs oranges at that particular moment.</div><div style="font-family:monospace" class="gmail_default"><br></div><div style="font-family:monospace" class="gmail_default">I can see use cases for both. For example, if I wanted to implement retry functionality, it becomes clear almost immediately that Exceptions are a much better vehicle for this then ADT's. Yes, you could easily retry if ServiceException or something. But we don't want to retry infinitely. Thus, you need to know which function and in which context we failed. And what if the second time we reattempt, we get further but get a ServiceException on the next statement? Start from 0 or continue on? What if the user wants the choice? All of this is way easier if you have a stack trace. Whereas for ADT's, the only (easy) way you could do that is if you pass in an instance of your ADT as a parameter of some sort (or a ScopedValue, now that that is an option available to us), and even then, you would end up recreating the concept of a stack trace. Exceptions give you a stack trace, they throw and rewrap with less ceremony, and all while uncluttering your happy path, even more than ADT's. ADT's make everything, success and failure, a first class citizen, which, like you said, forces everyone to hold the exceptional cases front and center, regardless of how important they are. If that hurts your API's ergonomics, well, you opted into that.<br></div><div style="font-family:monospace" class="gmail_default"><br>> Error handling is hard, no matter how you slice<br>> it.<br><br>I see that much more now. Still, I am making excellent progress in mastering that, especially in these past few days.<br><br>Thank you for your time and help!<br>David Alayachew<br></div></div>