[External] : Re: Better exception handling

Brian Goetz brian.goetz at oracle.com
Wed Mar 24 12:37:22 UTC 2021



>
> Another pain-point is the lack of composability of checked-exception 
> types over generics (or rather than exceptions don't have special 
> handling within generics).
>
> You can write...
>
>      ThrowingFunction<T, U, X> { U apply(T t) throws X; }
>
> But have no way to compose two functions with differing X1 and X2 
> without throwing away type-information, and no way to nicely elide the 
> 'X' when there is no exception (or no checked exception).
>
> I had wondered about an option 'throwing position' generic argument 
> and an | notation for composing the value
>
>     ThrowingFunction<T , U, throws X> { U apply(T t) throws X; }
>     ...
>     ThrowingFunction<Source, Intermediary, throws FailureOne> first = ...
>     ThrowingFunction<IntermediaryOne, IntermediaryTwo, _> second =  
> ... // eliding the throws is an option, Specifying Void or 
> RuntimeException is an option.
>     ThrowingFunction<IntermediaryTwo, Target, throws FailureTwo> third 
> =  ...
>     ThrowingFunction<Source, Target, throws FailureOne | FailureTwo> 
> chain = first.andThen(second).andThen(third);
>
> It would conceivably enable a version of APIs like 'Stream' API that 
> could propagate exceptions (and other useful constructs) - especially 
> if adding a 'throwing position' argument were type compatible with 
> callers eliding it

I spent a few hundred hours down this rabbit hole, with this very hope.  
Not only does it have all the downsides you describe (which I would 
describe as "high annotation burden"), this model kind of falls apart 
when you get to something like streams.  It works OK with a method like 
List::forEach, where the only thing the method will do with the lambda 
is call it in the same thread.  With such a restriction (and some help 
from the language), you could write a `forEach` method that lifts the 
thrown types from the lambda to the forEach method, so if you hand it a 
non-throwing lambda, the forEach call throws nothing.

But, this falls apart for streams.  Because, when I have a stream:

     list.stream()
          .filter(lambdaThrowingE)
          .map(lambdaThrowingF)
          .forEach(lambdaThrowingG)

I would have to treat the E as being incorporated into the return type 
of filter -- "stream that throws E when you eventually traverse it" -- 
and then when I get to map, return a "stream that throws E|F when you 
eventually traverse it", and then finally combine it with G when you get 
to the terminal operation.  The type system would need to be able to 
capture these things, and carry them through the pipeline.

The conclusion of this research is that it is _possible_, but if I 
showed you what the Streams declaration would have to look like, you 
would reasonably conclude "If this is the answer, I asked the wrong 
question."

But the fun keeps going.  If we bring exceptions into the generic type 
system (means we need variadic type parameters, combinators for & and |, 
and other fun), then we will eventually confront what does "catch E" 
mean.  I leave this fun as an exercise for the reader.

> Not only is the 'checked exceptions suck' opinion not universally held 
> within the Java community, but the lack of them in languages like C# 
> is even occasionally derided by users of stricter languages. Without 
> checked-exceptions, you're at the mercy of vendor documentation to 
> know which exceptions to catch - making that the compiler's job is a 
> strength in the language (especially if the modes of failure change 
> with a dependency version update).

No argument from me on this point.



More information about the amber-dev mailing list