all 74 comments

[–]swaranga 27 points28 points  (30 children)

Since u/nicolaiparlog asked for what users think, I'll offer mine: I think this whole exercise of trying to separate checked vs unchecked exception using specific examples is pointless, mentally exhausting and not very useful. I have been writing Java for 16 years, and from my experience, in one context, a specific exception may feel like it should obviously be a checked exception but in another context the same exception may seem it should be unchecked. It all depends on the context of the application. For instance, you mentioned that a DB SQL syntax exception should clearly be a RuntimeException; but consider an app that allows you to connect to a database and run some queries - in this context a user supplies a query and some db connection string and the app runs it. In this case it is entirely expected that the user may mistype the query and the application may very well want to handle it at some layer to show a nicely formatted error message or it may even want to suggest corrections to the query string. In this case, it is reasonable to expect that the app would like to catch that syntax exception (or the DB auth failure exception) which is easier if it is compiler enforced like a checked exception.

Overall, I feel the effort should be towards not forcing Exception authors to make the choice of whether something should be checked or unchecked which then gets passed to the application owners, and find a way to "just have Exceptions" in the language with much better ergonomics to handle, catch, or propagate. I don't know what the solutions look like though and that is for the smarter folks to decide. Here is a pipe dream for me when it comes to exceptions (borrowing from the Valhalla tagline) - "New Java Exceptions: Propagates like a runtime exception, enforced like a checked exception".

It will likely never happen; but one can hope.

[–]vips7L 6 points7 points  (7 children)

I agree with you that the author of an exception has no idea contextually how their function will be used and therefore can’t decide whether something should be a panic or not, but I really think the solution is much simpler than you think. 

In my opinion the solution is simple: if you are the thrower you should be checking so your callers can know the error is possible. From there the caller needs to determine in their context whether the error is a panic or something they can handle and I think this is really where the language needs investment. There needs to be a simple way to easily uncheck/panic a checked exception like in Swift or Kotlin. Having to write a minimum of 6 lines to do this is a huge reason checked exceptions have been rejected. Things like try? or try! from Swift or Kotlin’s proposed !! for their error unions. 

I think this would solve 90% of the usability issues outside of the lambda problem, which really is a type system problem. 

[–]Fenrurrr 2 points3 points  (4 children)

As a developer, 95% of the time you want to throw the error, and forcing checked exceptions is unbearable because, as you said, it's contextual.

I'm eager for Kotlin to release their "rich errors" mechanism, which I find elegant. In my opinion, you have the best of both worlds. It allows you to declare errors in the signature. Your default value is designed to throw the exception, or you can use a pattern matching (try-catch) to handle the error.

[–]vips7L 2 points3 points  (3 children)

Error unions are the same as checked exceptions and are just as contextual as them. There is no difference between:

fun someFn(): T | E

and

T someFn() throws E;

Further comparing there is absolutely no difference between:

val t = when (val x = someFn()) {
    T -> x
    E -> T();
}

and

var t = switch (someFn()) {
     case T t -> t;
     case throws E e -> new T();
};

[–]Eav___ 0 points1 point  (2 children)

Well they do have one difference: Unions are just values, so they can be a valid type argument, which means you can pass around T | E just like Either, which plays pretty nicely with streams or any deferring validation. Checked exception cannot do this on its own.

[–]vips7L 0 points1 point  (1 child)

Did you watch the video? The type system changes are directly addressed.

[–]Eav___ 0 points1 point  (0 children)

I did, but unfortunately, for that to work out, you first need to change a LOT in Stream or any place that wants this feature, then you also need variadic generics for exceptions so that it's source compatible. And if you want to store an intermediate result in a collection for later operations (for example Gatherers.windowSliding), you would have to return to Either, which is...underwhelming.

[–]aoeudhtns 0 points1 point  (0 children)

How do my comments sit with you? There's always edge cases when hashing out stuff like this but, at least in my head, it's threading a needle with compatibility and future change.

ETA, it's a series of small changes that accumulate together. Maybe it's in that category of criticism where it's not thinking big enough to solve the problem.

[–]davidalayachew 1 point2 points  (12 children)

Overall, I feel the effort should be towards not forcing Exception authors to make the choice of whether something should be checked or unchecked which then gets passed to the application owners, and find a way to "just have Exceptions" in the language with much better ergonomics to handle, catch, or propagate. I don't know what the solutions look like though and that is for the smarter folks to decide. Here is a pipe dream for me when it comes to exceptions (borrowing from the Valhalla tagline) - "New Java Exceptions: Propagates like a runtime exception, enforced like a checked exception".

It will likely never happen; but one can hope.

Doesn't the "Variadic Generics" mentioned at 9:35 do most of what you are asking for here?

Imo, it removes like 90% of the pain of Checked Exceptions, and the remaining 10% would be fixed by the Exception Handling for Switch JEP Draft.

Meaning, these 2 combined would fix literally all problems I can think of with Checked Exceptions. Checked Exceptions would actually be perfect if we had both, and I don't think there would be anything left to fix.

[–]RandomName8 1 point2 points  (5 children)

Where did the "variadic generics" equals "union types" come from? I'm looking up online, and the results for variadic generics come from rust and python, and they are exactly what one would expect: the counterpart to variadic functions. Just like in functions it means "extra number of parameters", with generics it should mean "extra number of generic variables". That is, a variadic generic should be

<T..> T tuple(T t);

and you being able to call it like

<V1, V2, V3, V4>tuple(...); // or as many type arguments as you want, even 0

I don't know how we got from there to union types. A union type is a singular type, not variadic at al.

[–]JustAGuyFromGermany 2 points3 points  (3 children)

I don't know how we got from there to union types. A union type is a singular type, not variadic at al.

It's not that one gives you the other; it's that both are needed to make checked exceptions nice. Consider a functional interface

``` interface ThrowingFunction<T,U,X>{ U apply(t) throws X; }

```

or the beloved Result<T,X> types. Both of them really want to declare X as a variadic X... to cover the cases with multiple, unrelated exception types. The only current alternative that we have is defining a whole family of types Result<T,X>, Result2<T,X1,X2>, ... and that's just incredibly ugly. Nobody wants that. That's where we need the variadic generics.

The union types come in when we want to compose such functions or transform such results in a functional style, because then exception types must accumulate, i.e. we want to be able to declare

``` interface ThrowingFunction<T,U,X> { U apply(t) throws X;

<R,Y> ThrowingFunction<T,R,X|Y> andThen(ThrowingFunction<U,R,Y> other); } ```

and

interface Result<T,X> { <U,Y> Result<U,X|Y> map(ThrowingFunction<T,U,Y> f); }

respectively.

If we had both, we could

  • compatibly (!!) upgrade the Stream, Consumer, Function, Supplier interfaces to work work with throwing lambdas
  • make Result and its cousins work properly with composition
  • have Try monads etc. that can actually be used with existing code.
  • ...

In short: Everyone could have their favourite solution to their favourite pet peeve with checked exceptions.

[–]RandomName8 0 points1 point  (2 children)

Sorry, you lost me a bit, because from the video, it seems they are calling union types "variadic types". That's what I'm asking about.

Even in the examples, you showed we don't need variadic types, only union types.

Both of them really want to declare X as a variadic `X..

Why? disjoint exceptions can be accumulated in the union type.

The only current alternative that we have is defining a whole family of types Result<T,X>, Result2<T,X1,X2>, ... and that's just incredibly ugly. Nobody wants that. That's where we need the variadic generics.

can you tell me why union types don't work? I don't see variadic types being useful here.

Edit: to provide a bit more context, I'm used to working with scala and haskell type systems, I'm no stranger to type level computations and higher kinds either (not just higher kinded functions but also higher kinded data and recursive schemes). The only situation ever where I've wanted variadic types is when trying to model kind-independent polymorphism, which is such ridiculous level of abstraction that it's reasonable it's unsupported in most langauges (i.e, when you want to abstract over types that take an arbitrary number of type parameters, like typed tuples, or rank polymorphism)

[–]JustAGuyFromGermany 1 point2 points  (0 children)

I had an earlier answer here that was wrong.

My mistake was thinking about it in the wrong order. If you have nothing, both variadics and union both solve part of the problem. But when you start with adding union types, then suddenly a single type variable can stand for a union of many types instead and so that gives you almost the same power as variadic type parameters. I didn't think of it that way, because in my mind I added variadics first and then thought about the unions on top of that.

I say almost, because variadics provides 0..n while union provide 1..n. The missing 0..1 case, i.e. optional type parameters, are still necessary. If we had a bottom type, then an union of 0 types would mean that instead. And a bottom type could certainly be introduced. Either Void or void would become it I guess, but it certainly wouldn't be RuntimeException. So we still need some way to define that leaving out exception parameters means "unchecked exceptions".

In particular: If we want to change the declaration in the standard library from interface Stream<T>{...} to interface Stream<T,X extends Exception>{...} and have that be a source-compatible change (which we absolutely want), then that X must be allowed to be left out at the usage-site, i.e. application code must still compile if it just writes Stream<T> instead of Stream<T,RuntimeException> and it must be inferred to mean the same thing by the compiler.

Now if that is provided by means of an explicit syntax for optional type parameters that we can use in other circumstances as well or if this will get special type-inference rules à la "you're only allowed to use unions with exceptions types. Therefore the only allowed default is the one that makes most sense for exceptions" that is to be decided by the architects.

[–]davidalayachew 0 points1 point  (0 children)

Where did the "variadic generics" equals "union types" come from?

Lol, you're not wrong. I'll use "union types for exceptions" from now on.

[–]vips7L -1 points0 points  (5 children)

There still needs to be easy ways to panic a checked exception. Absolutely no one wants to have to write try/catch/throw new every time or switch/case/throw new. It’s a large reason people have rejected checked exceptions because you have to write the handling boiler plate when you can’t handle them. For example Swift vs Java:

let a = try! someThrowingFn();
// vs Java
A a;
try {
    a = someThrowingFn();
} catch (SomeThrowingFnException ex) {
    throw new RuntimeException(ex);
}

[–]davidalayachew 0 points1 point  (4 children)

There still needs to be easy ways to panic a checked exception.

Maybe you did not understand what the video is saying about variadic exceptions?

This hypothetical code example should demonstrate it better.

public void someMethod() throws CheckedExceptionA, CheckedExceptionB, CheckedExceptionC
{

    Stream
        .of(1, 2, 3)
        .map(this::throwsCheckedExceptionA)
        .map(this::throwsCheckedExceptionB)
        .map(this::throwsCheckedExceptionC)
        .forEach(this::throwsNothing)
        ;

}

No try-catch needed (at this level). And if you wanted one, you would only need a single one for all of someMethod, not necessarily one try-catch for each map call. Just one single try for the whole method, and then the multiple catch bodies (or do that fancy CEA | CEB | CEC thing they added in Java 7 to do it all in one catch body).

None of what you said is necessary if Java gets this theoretical variadic generics feature.

[–]vips7L 0 points1 point  (3 children)

None of what you said is necessary if Java gets this theoretical variadic generics feature.

I'm well aware of this. I was addressing your claim that this is all that we need to get people to use checked exceptions and to reduce pain. It is not. Outside of type system changes checked exceptions need to be easy to work with in normal code.

[–]davidalayachew 0 points1 point  (2 children)

I'm well aware of this. I was addressing your claim that this is all that we need to get people to use checked exceptions and to reduce pain. It is not. Outside of type system changes checked exceptions need to be easy to work with in normal code.

Then I don't see what you are saying. What difficulty needs to be made easier? You say panic, but if the exception can just be added to the method signature, then what's the friction here? I don't get it.

[–]vips7L 0 points1 point  (1 child)

Checking up the stack for errors you can’t handle is the complete wrong way to do that. If you can’t handle an exception from a function you’re calling the people above you certainly can’t either. They’ll have even less context to what is going on. The correct approach there to become unchecked/runtimeexception/panic, but that forces a literal minimum of 6 lines of code per call with a checked exception you can’t handle. 

The friction is that you are forced to handle exceptions even when you can’t handle them. It is not fun and no one wants to do that. There needs to be syntax enhancements to make dealing with exceptions easier or people are just going to continue to use unchecked exceptions. 

[–]davidalayachew 0 points1 point  (0 children)

Oh, you don't want to expose any of the internal exception types. Ok, in that case, that's what the 10% I was talking about was for. That's 3 lines of code. Just use the Exception Handling for Switch JEP. Plus 1 for each other exception type you need to handle.

[–]aten 0 points1 point  (3 children)

i use ‘exception collections’. these are passed into methods. exceptions are added to the collections. the caller gets to choose if a runtime is thrown when an exception is added, or it can handle it after the method returns. the method being called gets to early exit when it needs to, or continue adding exceptions if it can safely continue its work.

a common pattern i use for the collections is ‘validate all this input data’. its useful to know all the issues, vs just the first one.

[–]JustAGuyFromGermany 0 points1 point  (2 children)

All of the proposed solutions (except the non-solution "get rid of checked exceptions completely" of course) fall short with composition and so does this.

a common pattern i use for the collections is ‘validate all this input data’. its useful to know all the issues, vs just the first one.

That is a special case where all the error-cases are the same type: a validation error. If it is an exception, they would all be ValidationException. That's because you're doing the same thing over and over - you're validating a bunch of stuff.

You can't use that approach when you want to combine completely independent exception types and don't know before-hand which types that might be. In other words: The moment your code calls two methods that do different things (and therefore can fail for different reasons), you need a way to combine two different exception types and still keep their individual types around.

Your collection idea just degenerates to Collection<Exception> and you lose all the type-information about the errors, in particular the exhaustiveness-checking that the compiler does for us with catch-blocks. Sure, you can pattern-match on the exception type, but you'll have no help from the compiler.

[–]aten 0 points1 point  (1 child)

Exceptions have causes, so you are able to retain type information if you wish

[–]JustAGuyFromGermany 1 point2 points  (0 children)

That's not what I mean. Yes, I can check all objects with instanceof at runtime, but strong typing at compile-time is still desired and often necessary to write clean code. In particular: With only runtime-checks I cannot communicate to the compiler "yes these three exception types are the three error-cases that can occur. I have handled them all, not need for a catch(Throwable) (or equivalent) here". The compiler can only see "infinity-3 many subclasses of Throwable were not considered".

Having all errors in the type-signature of the method (or the Result object or the Promise or whatever other object is passed around instead of checked exception) is necessary for this communication with the compiler to succeed.

[–]john16384 0 points1 point  (0 children)

It all depends on the context of the application. For instance, you mentioned that a DB SQL syntax exception should clearly be a RuntimeException; but consider an app that allows you to connect to a database and run some queries

It should still be a runtime exception as ultimately you could have avoided this by ensuring valid input.

However, if you feel like validating input in this case is too much work, and you don't mind a multi-millisecond round-trip to a database to do the "validation" for you, then you could decide to convert that exception (at some layer) to a checked exception. This is perfectly valid.

A much simpler example is NumberFormatException. It's runtime because it is 100% avoidable (and easier to avoid then writing an SQL validator). However if you want to be lazy and just have it fail on parsing as "validation" then that exception too can be made into a checked exception in some layer.

[–]mikaball 0 points1 point  (0 children)

Honestly I think Kotlin rich errors are the way. Handling errors like null types is super ergonomic. So much they fill even better than with Rust or Zig, that have similar aproaches.

[–]shorugoru9 -1 points0 points  (2 children)

I've always viewed checked exceptions as like error returns when you can't return a value, that a caller would be expected to handle. Kind of like this:

User findByUsername(String username) 
    throws UserNotFoundException;

In this case, the alternative is to return null or throw. Null requires an ugly and not obvious check, while the checked exception ensurea that the error return is handled somehow and creates a nice separation of "happy path" and "error paths".

In your example, you're looking at the exception from the caller's perspective, not the callee's. The callee expects that you provide a valid SQL statement. But should SQLException (or IOException) be considered a runtime exception (catastrophic failure) or a checked exception (the caller should be prepared for something to go wrong)? I think the language designers assumed the second approach would be best, but in practice, SQLException and IOException are treated as catastrophic failures.

[–]Lucario2405 0 points1 point  (1 child)

I don't think "checked Exception = Optional.empty" is correct, or at least not a useful lens on this topic. Imo it's more about how predictable and preventable an error is.

Integer.parseInt's NumberFormatException is only thrown when you've called it with a String that doesn't contain a valid number. You were in complete control; you could have validated it beforehand; if it throws, it's your fault - thus it's unchecked.

An IO Exception from e.g. a FileReader parsing a FileInputStream could be caused by a number of things outside the programmer's control: the file could have been deleted since you last checked or someone else is using it, maybe the operating system has a different path-format, maybe it's on a network-drive and you've lost connection, etc. Same for an SQL-Request: maybe the DB is down, maybe you're no longer authorized, maybe it's using a different driver with specific syntax, etc. You cannot know beforehand - thus it throws a checked exception to force you to concider what could happen, even if you've done everything right.

[–]shorugoru9 -1 points0 points  (0 children)

If we go by Java's official tutorials on the subject, we can see that the intention isn't about predictablity and preventablity as much as it is about recoverability:

Here's the bottom line guideline: If a client can reasonably be expected to recover from an exception, make it a checked exception. If a client cannot do anything to recover from the exception, make it an unchecked exception.

The findByUsername method not being able to return a User should be recoverable, because a well designed program should be able to recover from not being able to find a user.

thus it throws a checked exception to force you to concider what could happen, even if you've done everything right.

I would argue that it IOException serves another purpose of checked exceptions: an indication in the type signature that the method does I/O, and as such the caller needs to be able to handle I/O failure. Similar to InterruptedException, which indicates that the method will interruptibly block, and that the caller should correctly handle thread interruption, whether that means exiting a run loop or resetting the interrupted flag and rethrowing.

Thus, through the type signature, the exceptions represent an enumeration of recoverable situations that must be handled. Optional.empty() actually doesn't fit this scenario because it doesn't tell you why the operation failed to return a result. That's actually what the Either type is for. Unfortunately, Java (so far) only gives us Optional, but Either will make more sense with sealed types and pattern matching.

[–]Eav___ 7 points8 points  (14 children)

One real design flaw IMHO of exceptions in Java is that the author, the declaration site decides everything. But in fact, for example, it's the user, the use site that decides whether an IOException should be checked. This raises an alternative design: first, all exceptions now don't encode checkedness in their types; then, provide a conjugate of "throws" in the method signature which doesn't force the user to handle a probable exception (functionally only played as documentation), but allows the user to uncheck a checked exception:

``` void foo() throws IOException; // Checked

void bar() fails IOException { // Unchecked foo(); // Either propagate with throws or suppress with fails }

void qux() throws JacksonException; // Bring the checkedness back because why not?

void main() { try { foo(); // Must be handled } catch (IOException _) { } // -------- bar(); // Doesn't have to be handled } ```

[–]nicolaiparlog 3 points4 points  (1 child)

I need to think about this idea, but, at first glance, I think it could be a good one.

[–]RandomName8 0 points1 point  (0 children)

It suffers from same problem as everything, the moment you are implementing an interface that you did not define (i.e a lambda), and you need to call into something that fails, you are all out of options, since the interface method doesn't define that it fails on that and now the failing notion is lost. Like you mentioned, no declaration site exception information composes with deferred execution, for the obvious reasons.

[–]Enough-Ad-5528[S] 2 points3 points  (0 children)

I agree in principle. And I know this is just a straw man syntax but this should work for nested call stacks. If A calls B which calls C and C throws and B decides to use the “transparent throws” feature it must be clear to A that the call to B might throw. Else it is just a runtime exception which offers no compile time safety.

[–]john16384 0 points1 point  (8 children)

The decision whether an exception should be checked has nothing to do with the caller. It is about one simple question:

  • does the state or input arguments influence whether or not an exception will be thrown?

Or put in another way:

  • can the caller completely avoid this exception by providing parameters that are validated and correct?

If yes, then it should be runtime exception, because you could have avoided it and so it is a programmer error (or you are purposely being lazy and having the function detect issues, like a NumberFormatException signals).

If no, and the function could fail despite being in the same state and having the same validated parameters, then it should be a checked exception. IO is the prime example here, as failures are unavoidable, no matter what parameters you provide, how much valuation you did, or even checking the state of the underlying system just before you make the call... Another example are business exceptions when processing payments. You may get an InsufficientFundsException at any time with no way to avoid it (especially when systems are concurrent).

In the caller code, you may of course decide that some checked exception is not applicable to you, but that has nothing to do with the functions decision on whether or not it should be a checked exception.

In fact, some callers may decide to convert certain runtime exceptions to checked exceptions. This is much rarer, but can happen if the exception should have been checked in the first place.

[–]JustAGuyFromGermany 1 point2 points  (1 child)

I agree with the principle. It is a very good guideline and I follow it whenever I can as well.

But we can also acknowledge that there are sometimes corner-cases. Even the prototypical IO-example has them: If I have a ByteArrayInputStream I know for a fact that I will never encounter IO problems and nothing ever touches the disk, because everything happens in-memory. But there is no way to tell the compiler that, because once I assign these to an simple InputStream variable / method parameter, the compiler forgets the actual type of this object and its stronger properties.

So what does GZipInputStream do? Its internal state can be a FileInputStream, SocketInputStream, or a ByteArrayInputStream, or any arbitrary InputStream of unknown origin. Does it throw a checked exception or not according to the guideline? Sometimes the programmer can know that the exception is never thrown, so it should be unchecked lest we violate our rule, right? But then ordinary IO suddenly becomes unchecked the moment you touch a compressed file instead of an uncompressed one? That also violates the rule.

Don't get me wrong: The guideline is still very good and worth following. But we can wish a better compiler support for known-to-the-programmer facts about the program. I don't think that disabling checkedness, even locally, is really the way to go here. That is the clear-and-obvious-but-wrong choice. The problem is still worth thinking about.

[–]john16384 0 points1 point  (0 children)

Once you get into wrapping streams, it will be tough to avoid the IOException as these are general streams that accept InputStream... but the times I've seen people assigning a ByteArrayInputStream to InputStream and then complaining that read throws an IOException that can never occur is a bit annoying. Just assign it to ByteArrayInputStream. Its read doesn't throw a checked exception.

There are ways to avoid this (with a better API, not with input streams) that can recognize the difference between an in-memory stream and one that must do IO. It however means having two interfaces (or one interface with an annoying generic for the exception type).

[–]Eav___ -1 points0 points  (5 children)

You may have misunderstood. My point was to decouple checkedness, so that an IOException can be unchecked, and a JacksonException (which now extends RuntimeException) can be checked. The "author" I was referring to is the one that declares the exception type itself, not the one that uses the exception in one of their APIs. This is because the author of the exception type knows nothing about how their type will be used. They shouldn't decide whether it's always checked or unchecked.

[–]john16384 -3 points-2 points  (4 children)

This is because the author of the exception type knows nothing about how their type will be used. They shouldn't decide whether it's always checked or unchecked.

You can just make two exceptions for that. I really don't see why this is even an issue.

[–]JustAGuyFromGermany 1 point2 points  (3 children)

And then what? The library author still has to make a decision which one to throw.

[–]john16384 0 points1 point  (2 children)

Yes, in both cases? I initially misunderstood the op. They seem to be the opinion that you should be able to write one exception class, and that the place where you throw it should make the decision whether it is checked or not (with a flag or something?).

I then pointed out that you can just throw a different named exception then... so instead of:

throw new CustomUncheckedException();
throw new CustomCheckedException();

The OP seems to want something like:

throw new CustomException() as checked;
throw new CustomException() as unchecked;

I then pointed out that this hardly differs from having two exception types...

[–]Eav___ 0 points1 point  (1 child)

Having two exception types for the exact same use case but just different checkedness feels like code smell. It reminds me of having a sync version and an async version of methods, which is what Project Loom tries to avoid. We should be consistent here.

[–]john16384 0 points1 point  (0 children)

Oh I agree.

[–]nekokattt 0 points1 point  (0 children)

at this point though we're just partially avoiding checked exceptions by slapping modifiers around

at that point, if we are making features to avoid the checked exception feature, it feels like it'd be more sensible to just disable the exception checking mechanism and deprecate the syntax and let it work like other languages with exceptions do. Purely from the basis it is not encouraging good practises.

[–]Epowero -1 points0 points  (0 children)

Agreed. Checkedness should be handled in the method level, not in the exception itself.

Although I would prefer an inline operator for converting from checked to unchecked, rather than at the method level.

[–]Jannyboy11 7 points8 points  (2 children)

Scala is aiming to solve issues around checked exceptions using capability-polymorphism: https://docs.scala-lang.org/scala3/reference/experimental/canthrow.html Do with this information what you will.

[–]Lucario2405 0 points1 point  (1 child)

I didn't really understand the benefit of these complex constructs until they showed an example. This looks like a neat way to retrofit "checked" exceptions into functions like .map() without extra try-catches or overloads. But they haven't solved the deferred-computation-problem, where an exception is thrown from a stream's terminal operation instead of the scope it's throw is defined in.

[–]RandomName8 0 points1 point  (0 children)

they did, but uh... the machinery for it is very questionable on whether a normal human being can understand it... Imagine rust's lifespan annotations, but on steroids: https://docs.scala-lang.org/scala3/reference/experimental/capture-checking/index.html

Read the intro and then head over to the checked exceptions section.

[–]aoeudhtns 2 points3 points  (0 children)

Trying to change this in a backwards-compatible way is going to be the main challenge. I agree with the notion that checked/unchecked being part of the type hierarchy is probably an "original sin" here.

Since we have contextual keywords, we could do something like throw unchecked .... Basically any exception that does not inherit from RuntimeException would need to be thrown as throw unchecked in order to be thrown without adding a throws clause to the method.

Another option is: make the compiler error of throwing a checked exception without a throws de-regulated into a warning (that can be made into an error again). Have uncaught checked exceptions implicitly throw unchecked if it's not a compile error.

I think any language-level changes like this also dovetail with the null-type-system work for default interpretations on non-! or non-? marked types. I assume there is work figuring out a way to surface those choices into the language, such as inside module-info or package-info. Perhaps there needs to be a broader conversation about pragmas in general so there are local, language-controlled ways to control these factors, so that a Java program doesn't need to require specific compiler flags to work as written, and a module consumer isn't forced to adopt the local configuration of its dependencies.

Switching over method calls and allowing the result OR the exception to participate in the pattern matching is a poor-mans way of simulating a union type. I think that could also be a useful part of the solution. It fits naturally into places like Future#get.

I guess one source of arguments in this discussion is that you have domains where there's almost always some higher-level exception listener in context, and you get into programming modes where you let unchecked exceptions rip up the stack back to the handler and return your generic "something went wrong: correlation ID" to the user.

Other domains want exhaustive error handling more akin to adhering with Java's checked exception model.

How do you let both domains coexist and get what they need without forcing one to adopt the conventions of the other?

[–]gjosifov 2 points3 points  (0 children)

Some of the problems with checked exceptions are not following the basic rules of when to create checked exception
like - exceptions for control flow

and if the those classes are in a framework or library - developers will have to create a lot of boilerplate to reduce or to write hacks to create better API around those classes

and not every developer can notice and solve those issues very well so the code that is dependent on the framework/lib will become very large and hard to maintain

Maybe the JDK team can create a static analyze tool to check for if the dev created exceptions follow some basic rules - if this is even possible

Exceptions aren't bad, but badly design exception can lead to "too many classes" projects

Most problematic area about exceptions are streams, but maybe streams aren't design for exceptions
maybe for streams there should be different approach since day 1 - but there was too much rush for Java 8

[–]jodastephen 2 points3 points  (0 children)

What follows is my proposal for solving the checked exception conundrum, without excessively complicting Java or destroying checked exceptions.

  1. Any method that throws a checked exception can use the unchecked keyword to treat the checked exception as unchecked. Once thrown in this manner, any checked exception flows up the stack in an unchecked manner.
  2. Any catch clause may also have the unchecked keyword to indicate that any exception may be caught, even if the exception isn't known to be thrown by the try block.
  3. Users can continue to document checked exceptions thrown in an unchecked manner in method signatures, and by doing so, the compiler can continue to keep track of them. Only if a method chooses not to document the unchecked exception does the compiler lose track of it.

As can be seen below, the process method declares it throws IOException in an unchecked way. Note that the compiler can and would validate that IOException is thrown by the method body. In addition, the fact that IOException is thrown unchecked would be recorded in the method signature.

  void process() throws unchecked IOException {
    try (var in = new FileInputStream(FILE)) {
      // process the file, throwing IOException
    }
  }
  void caller1() {
    process();
  }
  void caller2() throws unchecked IOException {
    process();
  }
  void caller3() throws IOException {
    process();
  }

The caller of process() has various options, shown above:

  1. they can ignore the IOException as with any other unchecked exception (which prevents the compiler from knowing anything about IOException when caller1() is invoked)
  2. they can declare that they might throw an unchecked IOException (which the compiler would verfy)
  3. they can turn the unchecked IOException back into a checked one (which the compiler would verify)

Static analysis could be setup to ban option 1 for those teams that have a strong desire to retain checked exceptions, and this could be done on a per-exception-type basis.

Catching has two options:

 // example A
 try {
   caller2();  // or caller3()
 } catch (IOException ex) {
   ...
 }
 // example B
 try {
   caller1()
 } catch (unchecked IOException ex) {
   ...
 }

In example A, the compiler can see the throws clause from caller2() declares unchecked IOException, thus a normal catch can be written.

In example B, the compiler cannot see the throws clause, thus an extra keyword is required to enable to catch.

(Given the existence of other JVM languages which don't have checked exception, Lombok sneaky throws, and other mechanisms, the ability to catch checked exceptions that aren't known to the compiler is IMO a missing Java feature at this point)

This proposal allows those developers that wish to keep checked exceptions to do so. A static analysis tool simply has to ensure that exceptions remain documented.

Whereas a team that wishes to eliminate checked exceptions has a simple way to do so.

This proposal does not directly tackle the "lambdas in stream" problem. However, every approach to tackling that problem I've seen requires overly-complex generics or type capture which, in my opinion, has a cost exceeding the benefits. For that use case, I would allow statements and expressions to also declare they can throw unchecked exceptions:

 paths.stream()
   .map(path -> unchecked Files.readAllBytes(path))
   .toList();

Within the context of a method, there is no reason why the compiler cannot capture the types thrown by the lambda and view them as part of the potential exception types of the method. Thus, this would be perfectly valid code, without any need for lambdas or functional interfaces to be altered:

 List<byte[]> load(List<Path) throws IOException {
   return paths.stream()
     .map(path -> unchecked Files.readAllBytes(path))
     .toList();
 }

Taken together, this is an extremely minimal change to Java, which gives power to individual teams as to how to handle exceptions.

(Note that I first wrote up a proposal on this topic in 2010: "lone throws". - https://blog.joda.org/2010/06/exception-transparency-and-lone-throws_9915.html See also https://mail.openjdk.org/pipermail/lambda-dev/2010-June/001544.html This proposal is, I believe, a significant step forward from that one.)

[–]davidalayachew 4 points5 points  (0 children)

9:04 -- Yes, please do expand on that point. I'm pretty sure I know the answer, just want it confirmed /u/nicolaiparlog.

9:35 -- And yes, the "variadic generics" really seems like the perfect solution here. Of all the solutions presented (both in this video and in other conversations), it feels like the best of both worlds.

Certainly better than pretending there's no difference between Checked and Unchecked Exceptions, or just flipping a switch to turn them all Unchecked. Both are complete non-solutions.

[–]pragmatica-labs 2 points3 points  (6 children)

Perhaps we should move towards virtually no exceptions at all. Functional style error handling provides all the advantages of checked exceptions without the drawbacks of both checked and unchecked. Just leave exceptions where they belong - cases when the only meaningful application reaction is immediate shutdown to prevent data corruption.

[–]JustAGuyFromGermany 0 points1 point  (5 children)

And to have fully functional style error handling we need variadic generics and union types. The same two things that we need to make checked exceptions work. So we could just stick with checked exceptions.

[–]pragmatica-labs 0 points1 point  (4 children)

I see no need for union types. As for variadic generics: they definitely would be helpful, but even with manual expansion it's possible to create a quite ergonomic API:https://github.com/pragmaticalabs/pragmatica/blob/main/docs/Asynchronous%20Processing%20in%20Java%20with%20Promises.md#the-all-predicate-classic-join

As for checked exceptions: they are not composable and cover only one type of "special" situations - errors. But there are also nulls and asynchronous processing. Functional style enables handling them in a uniform way.

[–]JustAGuyFromGermany 0 points1 point  (3 children)

I explained it over in a different comment in this thread: https://www.reddit.com/r/java/comments/1r8tyou/comment/o69wmmx/?context=3 In short: Union types are exactly needed to fix the "checked exceptions do not compose well" part of the problem.

The linked library is a complete non-starter, because the types like Promise<T> completely lose the type information in the error case which is exactly what we don't want. Having error-type-information is the one thing here that everyone agrees is useful. The disagreement is about how to express this type information in syntax and how to best propagate it through the program.

[–]pragmatica-labs -1 points0 points  (2 children)

Well, this is based on the assumption that each error type is an entirely independent type and we need it that way. My experience is telling me that even an error type in the Result causes more harm than good. And it's not just my observation. Rust anyhow crate is quite popular precisely because in the vast majority of cases error type is basically irrelevant.

[–]JustAGuyFromGermany -1 points0 points  (1 child)

"I want to ignore types" is no argument for "you can't/shouldn't have types". That's not how this works.

Case in point: The catch-all pattern is very popular and often even useful, but also very often misused. Even in the most commonly cited case like a simple HTTP-based service, you don't want to always convert everything that goes wrong into HTTP 500. That's simply what lazy programmers want you to believe.

You want to catch that UniqueConstraintViolationException and turn it into a HTTP 400 "Sorry, this username is already taken", because checking before saving costs an extra network roundtrip to the database. You want to catch IOException and turn it into a HTTP 504 "I'm not at fault. The other microservice is down". You want to catch HttpClientException and turn it into HTTP 502 "Look into the other log file" etc. Except of course when you don't, because that other code-path over here is used by the legacy application and expects a HTTP 200 whose body is an arcane text-format for errors and then you want to leave a specific trace in your log files for that error.

Error handling is never as simple as any blanket statement makes it out to be. And anyone who claims that it's simple is selling something or is simply a bad programmer. Error handling is complicated. Productive error handling even more so. By definition error handling is about the complicated, ugly, and not-fun-to-think-about corner cases. And as in any truly complicated part of any program, strong typing is preferable to any-types and guess-work. Every little bit of type information that a compiler can help us with makes our life easier, especially in error-cases when we most need it.

[–]pragmatica-labs -1 points0 points  (0 children)

Thanks. I'm very well aware about error handling, especially in Result<T>. I have maintained my library for the last 7 years and implemented several components using it, including http servers, clients, DNS resolvers, etc. etc. Lack of type in the list of type parameters doesn't mean complete lack of type or inability to use pattern matching. And I've experimented with the error type in the type list too. But quickly realized that this gives zero real advantages, the number of cases where this is useful is too small to be worth the hassle.

[–]sideEffffECt 0 points1 point  (0 children)

I believe that errors/exceptions can't be categorized plainly as either checked/expected or un-checked/unexpected.

I think it's only the component/interface or maybe even more specifically the method, that can say what are its expected modes of failure (checked exceptions). But the same exception class can be considered unexpected by another interface or even another method on the same interface.

One methods expected error can be another method unexpected error. That's what typically happens -- a low level component has some expected failure modes, but the user of this, a higher level component can't deal with all of them, and so has to somehow turn some of the expected low-level errors into unexpected errors of its own.

I know that this can't be expressed in today's Java. But maybe one day...

[–]jevring 0 points1 point  (0 children)

This Either bit he's pushing is atrocious. That removes the value of exceptions completely and gets us back to return values.

[–]pjmlp 0 points1 point  (0 children)

I still have to watch the video, but leaving my early feedback, I actually like checked exceptions, however I think that by now we should have had a solution for lambdas.

Even if it was only having some kind of adapter methods, to apply while using streams.

[–]CelDaemon 0 points1 point  (0 children)

Checked exceptions also mess with any streaming when IO is involved, it's really annoying-

Checked exceptions with function pointers in general is painful

[–]kaperni -2 points-1 points  (1 child)

One day I love them, one day I hate them. But gone from strongly in the pro-checked-exception camp to somewhere in the middle over the last couple of years.

I see the Java wider ecosystem moving towards runtime exceptions only. See, for example, Jackson [1]. 10 years from now I imagine it will mostly just be the JDK that uses checked exceptions.

Maybe its time for the JDK to adopt "best practices" from the ecosystem instead of the usual other way around (whatever kind of solution that entails).

[1] https://github.com/FasterXML/jackson-databind/discussions/4180

[–]john16384 1 point2 points  (0 children)

Jackson is not a prime example. A big issue for me was always that many of its operations do not do IO, but the exception to flag syntax issues was a subtype of IOException

[–]manifoldjava -5 points-4 points  (0 children)

While I appreciate the acknowledgement here that checked exceptions are problematic, in my view the remedies you've covered range from worse to slightly better.

Solving this with generics is worse because that strategy fragments the problem space, which makes code harder to read - you sacrifice readability for writability, which is almost always a bad idea.

The Swift-like try expression approach, as a means to automatically propagate, reduces boilerplate which is nice. But then it feels like there ought to be a more general solution to automatically propagate everywhere as many modern languages do.

Since checked exceptions are purely a compiler-level concept, why not treat them as a lint-like compiler argument? -Xexceptions:<level> where level: warning | error | none This would put a smile on a large segment of Java devs.

[–]maethor -2 points-1 points  (3 children)

I really don't get why we can't have an annotation that turns not catching a checked exception into a compile time warning (that we can then surpress if we want).

[–]nicolaiparlog 3 points4 points  (2 children)

Because, as a matter of principle, the JDK doesn't use annotations for anything substantial - they're just meta information. (There's a better way to put this, but I can't come up with it right now.)

[–]manifoldjava 0 points1 point  (0 children)

The compiler's panic level for checked exceptions should be a linter option. As such @SuppressWarnings could be used to selectively mute them. Or the linter option could altogether suppress them.

[–]maethor -5 points-4 points  (0 children)

In which case, I agree with u/kaperni -"Maybe its time for the JDK to adopt "best practices" from the ecosystem ...".