Hacker News new | past | comments | ask | show | jobs | submit login
JEP draft: Exception handling in switch (openjdk.org)
130 points by vips7L 7 months ago | hide | past | favorite | 103 comments



This will really help using streams and just being expression/HOF-oriented in general, I often find myself writing custom FunctionalInterface's just to capture an exception instead of writing a Consumer/Function/etc. maybe this will help ease that a bit. I still wish there was a way to just "bail out" execution of a lambda like you can a for loop if the function the lambda is used in is declared to throw the exception in question.

But I often find that I'm not thinking functional enough if I find myself trying to do that. But also sometimes I'm forced to by surrounding APIs etc.


My approach lately is either:

- Ban exception propagation altogether: in Kotlin, wrap the computation in 'runCatching' to get a Result<T>; or in Java, use result4j [1] which provides similar functionality.

- Let exceptions propagate, catching them at as high a level as possible.

Which one to go with depends on the type of program I'm writing. If it's a GUI tool, I go with the first approach, because I want to display errors to the user. If it's a CLI tool or backend service, I go with the second option, because I want to short-circuit the program as soon as possible to avoid potential logic errors.

[1]: https://github.com/sviperll/result4j


> - Ban exception propagation altogether: in Kotlin, wrap the computation in 'runCatching' to get a Result<T>; or in Java, use result4j [1] which provides similar functionality.

‘runCatching’ was never intended for user code. It was made for internal use in coroutines machinery and when community complained they made it public. But it is still a half-baked API that is better to avoid (catching CancellationException, doesn’t compose well in general).

The whole runCatching/billions of Result<T> copies are worse than Go’s “if err !=“.

Unless JetBrains rolls out language support for something like Rust’s Result, exceptions are superior and Kotlin community would better off embracing them instead of trying to be different for the sake of being different.

https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/sh...


> Unless JetBrains rolls out language support for something like Rust’s Result, exceptions are superior and Kotlin community would better off embracing them instead of trying to be different for the sake of being different.

If this was true, why do Jetbrains themselves keep reinventing their own per-domain Result classes for their new code? For example, in their coroutines library they invented another Result type specifically for Channels[1] and isn't interoperable at all with their other Result type[2] that was also originally written to facilitate some of the coroutines code.

If Jetbrains' own internal library authors keep reaching for a Result type then I think we're better off following suit, because if they thought exceptions were superior in this example, they would have used them.

[1] https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-corout...

[2] https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/


I have a hard time agreeing that exceptions (as they exist in kotlin, unchecked) are superior. There is no way to communicate (outside of docs) to callers that a function is possibly unsafe and that they should handle it.

Good callout on the stdlib `Result`, a lot of people aren't aware of the `CancellationException` issue. Another pain point is that the error is constrained to `Throwable`, which is rather obtuse for general business logic (and you're likely generating stack traces needlessly unless you disable `writableStackTrace` on your custom throwables).

I'm a fan of https://github.com/michaelbull/kotlin-result which has a fantastic API and supports monad comprehension which helps avoid the "arrowheads" from not having a built in operator.


That result library doesn’t look thread safe at all. I would be extremely cautious using it.


Author here. I have no idea what you could possibly mean with this comment. The coroutineBinding implementation correctly uses the coroutines API for parallel decomposition of Result bindings, exactly how the Kotlin Coroutines guide tells you to, backed by a Mutex[1]. The coroutineBinding isn't even the main selling point of the library, you can use it without using this feature entirely.

Please could you elaborate on what "looking thread safe" means to you? The only portion of the library that supports concurrency *is* thread safe - the unit tests[2] prove it and the use of concurrency primitives such as Kotlin's Mutex[3] are indicative of this.

I truly have no idea how you've judged the entirely of the library on whether it's "thread safe" when there is a single function (in an extension library, not the core one) that's related to concurrency and it is very clearly using concurrency primitives as intended.

With regards to "being cautious using it", you don't need to be. The maven central statistics suggest its being downloaded 300,000 times per month. If there was something wrong with it, it's likely somebody would have raised this already given how frequently the project has been adopted over the last seven years.

[1] https://github.com/michaelbull/kotlin-result/blob/master/kot...

[2] https://github.com/michaelbull/kotlin-result/blob/master/kot...

[3] https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-corout...


> Let exceptions propagate, catching them at as high a level as possible.

This can't be done cleanly in Java.

Checked exception can encode union types, but this extra power is not complemented anywhere else in Java's type system. E.g. in a `Consumer` lambda passed to `forEach`, Java's checked exception forces you to convert that to a RuntimeException.


I'd say it is due to a lack of variadic type parameters. You can write something like this:

    <T, X extends Throwable> void forEach(ThrowingConsumer<T, X> f) throws X;
But the only way to have multiple exception types without losing static type checking is to have multiple X parameters, like X1, X2, X3... (with unused parameters being set to some subtype of RuntimeException so that they do not participate in checked exception handling).

Whether or not it is worth to write this madness just to satisfy one's OCD is up to the reader.


> it is due to a lack of variadic type parameters

To be pedantic, it is due to union types "not complemented anywhere else in Java's type system". Adding variadic type params is a way to solve this. Another way is, of course, to support union types.

> with unused parameters being set to some subtype of RuntimeException

Or the `Nothing` type (`never` in TypeScript), where `A | Nothing = A`.


using RunCatching is inadvisable, it catches too much and can break lots of things in hard to detect ways

https://github.com/sksamuel/tabby/blob/0fa37638712efd6b059f2...

(though I recommend using the Arrow library, it has great types for fixing all the countless foot guns Kotlin devs insist on adding to the language and native libraries)


I tend to do the former only when writing executors, task queues, retry strategies, batch handlers, and the likes, otherwise rely on 2) (the UI is just another "high level")


I mostly work in the backend and yeah, the second approach is what I value the most.

Particularly because it can provide a ton of useful context (if logging is correctly setup) and doesn't end up littering the code with try/catch blocks and stuttered logging.


Already you can uncheck exceptions by wrapping them in a function like the ones defined here

https://paulhoule.github.io/pidove/apidocs/com/ontology2/pid...

that is

   something.stream().map(unchecked(SomeClass::methodThatThrows))


I don't agree with reasoning to not call it "case catch ...":

    case catch would be asking "did it evaluate to something that catches this exception?", which doesn't make sense.
Interpretation as "did it evaluate to something that would be catched like this?" makes perfect sense and is way more intuitive. Even "catches" would be better but why introducing new keyword?

"throws" is way too close to an action of throwing.


You're matching with the result of the expression inside the switch, and in case of an exception the result is something being throw, not something being catched, so the chosen approach seems correct to me.

given the code

    Future<Box> f = ...
    switch (f.get()) {
        case Box(String s) when isGoodString(s) -> score(100);
        case Box(String s)                      -> score(50);
        case null                               -> score(0);
        case throws CancellationException ce    -> ...ce...
        case throws ExecutionException ee       -> ...ee...
        case throws InterruptedException ie     -> ...ie...
    };
I read the above as

"if f.get() is a Box(...) then ..."

"if f.get() is null then ..."

"if f.get() throws ExecutionException then ..."


Further bikeshedding this, since the JEP admits regular clauses and Exception clauses should be kept separate as they are treated differently, we could as well retain the original catch syntax:

    switch (f.get()) {
        case Box(String s) when isGoodString(s) -> score(100);
        catch InterruptedException ie           -> ...ie...
    };
Current try/catch gymnastics are laborious, requiring blocks making usage unwieldy in otherwise-one-line lambdas. Requiring "case throws" is yet more useless syntax inflation. It would be nice to keep things streamlined this once.


Yeah, reusing `catch` was also the first thing that came to my mind. The "case throws" feels awkward even though I can see why it's proposed (can be read as "in case (it) throws)


catch clauses in try/catch are also matching something being thrown.

People don't have problem reading try/catch, are used to it, it's already there, semantics match - why complicate things?

Case is better read as "captures ... [when ...]" and "case catch" simply means capturing an exception - the same way as try/catch does it.

If you flip it around - if somebody would do an experiment where they'd ask 100 developers to imagine that java supports catching exceptions in switch statements my bet is that almost all, if not all would write it as "case catch ...".

And there is really nothing fundamentally wrong with that. You can't catch catch handler, you can only catch an exception.


> People don't have problem reading try/catch, are used to it, it's already there, semantics match - why complicate things?

We don't have a problem reading try/catch because it comes from an era when Java was more procedural and less functional. "case throws" makes more sense in the functional-style Java era. (Well, makes more sense to me at least)

EDIT: To clarify "catch" is a verb and contains a block of executable statements. "case throws" maps to an expression (because I assume most people writing new code that uses this will be using switch expressions, not switch statements) In this context, "case throws" is a better choice because you are talking about the _expression_ inside the "switch".


From my reading, they wanted it to match the sort of grammar that type constructor/type cases have. (looks like a declaration) I have not read the mailing list discussion to see what other alternatives were suggested.

My kneejerk reaction would be to prefer something like "threw" in place of "throws", but I'm sure there's a reason not to.


> I'm sure there's a reason not to

Language designers try to avoid adding new keywords if at all possible, especially ones that are short and common. Doing so requires either breaking thousands of projects and APIs or adding a bunch of complexity to the grammar and syntax highlighting tooling (so threw is only a keyword in some contexts but not others).


Of the two, "case catch" is The Correct Answer™. Obviously.

I would even accept just "catch", such that a switch can hold a mix of "case" and "catch" clauses, which would be most natural.


English isn't my native language but I read that as "in case it throws this type of exception" so "case throws" makes perfect sense to me.


But the throws keyword means "this can throw", it's not used to actually handle exceptions at all. Exception handling always uses catch. I think breaking that pattern would be a terrible idea, even if you can make sense if it with English grammar


Make it "case threw" then lol. But then this isn't the first time a keyword in Java means different things in different contexts. Like "final", which means that a field or variable can't be modified but on a class it means it can't be extended and on a method that it can't be overridden.


I like it. `switch throws` looks like a really clean construct.


> case throws Exception e -> { log(e); yield score(0); }

... and this is why people so frequently break thread interrupts in Java. most Java sample code (and even official-feeling docs like this) violates basic exception hygiene.

---

edit: that aside, I can see lots of uses for this pattern, and I like the clear scoping quite a bit. Seems like a good idea on the surface at the very least - I don't have enough experience here to really make a "good idea or no" claim.


This complaint is not really about the proposed change, it is a longstanding problem with Java. As long as you treat exceptions with the sane care in switch statements as you do in try statements, this doesn't introduce any new problems.


> it is a longstanding problem with Java

it's, along with null pointers, probably the no 1 biggest foot gun in the language, responsible for countless bugs in pretty much every app ever written in the language, and it's treated as some unaddressed elephant in the room we don't talk about or consider alternatives to


It is easy to put the right behavior in a function wrapper like

https://github.com/paulhoule/pidove/blob/97f8ec697d2890f13ba...


C# added nullable reference types a few years ago. Is there an equivalent in Java? That makes dealing with null pretty much a non-issue

https://learn.microsoft.com/en-us/dotnet/csharp/nullable-ref...


> and this is why people so frequently break thread interrupts in Java.

By the way. I've been writing predominantly Java over the last 15 years and I absolutely hate it that InterruptedException is checked. It gets in the way All. The. Damn. Time. All for those 0.1% of cases when you need to be able to cancel a blocking operation from another thread.


Super cool.

Best part about this language proposal format is how it spells out goals and non-goals so clearly. Having the non-goals, and dialing them in, is really great.


Excellent. I love Checked Exceptions, and this just made them that more useful. Can't wait for it to land.


[flagged]


`Either<MyError, Foo> foo()` and `Foo foo() throws MyError` and are pretty much isomorphic.

https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/...


Ignoring the much more annoying syntax needed to handle exceptions, I'd agree. But that's not the problem: you never know if foo() throws things other thank MyError. So in practice you still need an exception catch-all after your catch for MyError.

After years of Java, handling errors in idiomatic Scala with Either was a breath of fresh air. (And you can even wrap things that might throw in Try() to save yourself the headache of a try/catch tree.) Rust did it right with Result, and leaving out exceptions entirely.


I watched a video from Martin Odersky (creator of Scala) and he was sort of the same opinion when talking about effects.

    A b() throws C;

And

    A b()(using Throws[C]); 

are exactly the same.


Throws are nonlocal control transfer, not types. The interaction with the rest of the language is entirely different.


How are throws non-local? They simply pop a stackframe, the exact same way as the `return` keyword. Then the calling method gets to decide what to do, exceptions just have an implicit “unwrap-or-return/bubble up”.

Exceptions are nothing like gotos/long-jumps, it’s just “pressing escape key once vs keeping it pressed”, basically.


Rather than looking at what the (abstract) machine does

- throws are nonlocal control transfer

- result types are adding a tag to the data or error

Think about whether the two can express the same things, and whether one can be translated to another without losing information.


> Foo foo() throws MyError

Why did you link to Kotlin? It doesn't appear to have `throws MyError`.


You're right that was confusing.

For this line, where the same can be said for Java.

> Notice, that monad comprehension over Try monad is basically built into the Kotlin language.


...which is even worse, because now the compiler is saying "when you call this, you will get this" and you have NO way of knowing that the call actually returns any number of exceptions, as a matter of course, without reading EVERY line called by EVERY line in that method

vs just plainly indicating you will get Either<Error, Foo>


What are you talking about? You literally can with sealed classes.


I'm talking about the fact that every function call in Kotlin can return an exception (and it will always be undeclared, because Kotlin doesn't have checked exceptions)

this has nothing to do with sealed classes


they're in no way even remotely similar

Unchecked exception

    Foo foo = myClass.getFoo() // throws exception, but doesn't tell you, and nor does the compiler
Checked exception

    // now we have to do this everywhere, in every layer, lol (or put throws)
    try { 
      myClass.getFoo()
    } catch(Exception e){ //now we're catching ALL exceptions, yay
      // do what? who knows
    }
Sane Either/Option

    Either<Error, Foo> foo = myClass.getFoo()
^^^ the compiler disallows treating this as a Foo - but also doesn't force us to handle anything, we can pass this around without handling anything, and only when we want to, call map, flatMap, fold etc

Notably, it's just an object, it doesn't require weird special syntax, halting execution, hierarchies of exception (which mean you catch too much or too little etc etc etc)

it's objectively better on every metric except familiarity

but since Java devs are stuck in the 90s and refuse to listen to even their own language designers about how traditional Java ways of doing things are fundamentally broken (don't take my word for it, go listen to what Josh Bloch and Brian Goetz say), they refuse to even consider adopting it until they're forced to (like when Java introduced Option) (which itself is the worst implementation of that type in any language, but that's a side note)


> it's objectively better on every metric except familiarity

I find that as a rule, people who throw around the word "objectively" a lot tend to dramatically overestimate how much they know, usually because they're advanced beginners in the domain that they're so confident in and haven't yet run into the complexity of real-world implementation.

There are very few features in programming languages that can be described as "objectively" better or worse than other features. The whole discipline is more art than science, and the only practitioners who actually accomplish anything meaningful in their work are the ones who acknowledge the inherent subjectivity of most of programming language design.

Programming languages do not exist in a vacuum, they exist in an ecosystem of interrelated individuals and technology. Do not be so quick to dismiss the weight of generations of programmers and billions of lines of code as people just being "stuck in the 90s".


> the weight of generations of programmers and billions of lines of code as people just being "stuck in the 90s".

The weight of generations of programmers and billions of lines of code have come to the conclusion that:

1) Functions should declare/document what they return (including failure modes)

2) Checked exceptions are not worth it

These are mutually exclusive.

Result types genuinely are better.


I'm a fan of Result types in languages that incorporated them as part of the design (say, Rust), but I'm not convinced that they're the right answer for Java. Result types only really make sense in a highly expression-oriented language, and while Java has taken important steps in that direction it's really not there yet and likely never will be.

I think a more fruitful path of exploration is improvements to the ergonomics of checked exceptions. Semantically there really is very little difference between checked exceptions and Result types (OP's argument to the contrary is a straw man and proves nothing), the main reason why people fought their use in the early days of Java was that the ergonomics were pretty poor. I suspect that there are a few tweaks that could be made with modern PL techniques that would make checked exceptions feel more comfortable (analogous to the addition of ? in Rust), and since they've been baked into the language for decades now through thousands of existing libraries the returns from making them easier to use will be much higher than the returns from creating a new "standard" way to handle errors in Java.


> Result types genuinely are better.

Only if there’s a language support for them. Like Rust, where I can concisely drop out of function if it’s an error case.

https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/sh...

If I have to constantly write

when(result) { Ok -> … Err -> … }

then checked exceptions and throws clause are better.


Normally I'd agree with you, but this really is not a matter of opinion, it's up there with eg compiler built in null safety is superior to everything being able to be null. There's plenty of people who think it's not, and who come up with all kinds of bizarre reasons to say so, it's just that they're wrong. Even the Java language designers themselves (including people like Bloch and Goetz) have for decades been talking and writing books about how wrong Java got a lot of things. But the community is stuck in its ways and wants to keep coding like they always have.


> Even the Java language designers themselves (including people like Bloch and Goetz) have for decades been talking and writing books about how wrong Java got a lot of things.

Can you please provide a citation, with a link, for where Bloch or Goetz said that checked exceptions are something Java got wrong?

Effective Java has a lot to say about exceptions, and does encourage avoiding them where possible, but you have to really be cherry picking to read it as Bloch saying to never use checked exceptions when the 2017 edition says this:

> The cardinal rule in deciding whether to use a checked or an unchecked exception is this: use checked exceptions for conditions from which the caller can reasonably be expected to recover. By throwing a checked exception, you force the caller to handle the exception in a catch clause or to propagate it outward. Each checked exception that a method is declared to throw is therefore a potent indication to the API user that the associated condition is a possible outcome of invoking the method.

And this:

> Many Java programmers dislike checked exceptions, but used properly, they can improve APIs and programs. Unlike return codes and unchecked exceptions, they force programmers to deal with problems, enhancing reliability.

Bloch advocates against overuse of exceptions, which is fair, but his reasoning applies equally well to overuse of Result: if you can use the type system to prevent an invalid value from being passed in, do so!

As for Goetz, this is the most recent commentary I can find from him on checked exceptions [0]:

> If people are not forced to document what errors their code throws, they won't -- especially the people that the people in camp (3) are afraid of. As long as those folks are allowed to code, the value we get from checked exceptions forcing developers to document failure modes overwhelms the annoyances.

> But, as I said above, I think many of the annoyances can be removed by adding a small number of exception streamlining constructs. This doesn't help Neal with simplifying closures, but it does help us get our job done with a little less pain.

> Finally, a meta-note -- its really easy to misinterpret the volume of support for "removing checked exceptions" as any sort of gauge for community consensus. We're intimately familiar with the pain that checked exceptions cause; we're substantially less familiar with the pain that they free us from. (Obviously, neither approach is perfect, otherwise there'd be no debate.)

[0] https://nofluffjuststuff.com/blog/brian_goetz/2007/06/remove...


I was referring to how Bloch and Goetz have tried to tell Java devs for decades to avoid null, mutation and just in general the traditional Java coding style, with little success.

As for exceptions, it's hilarious that they and the Java community in general are still stuck on arguing about checked vs unchecked exceptions. That's my whole point - you can argue about that forever and you will always be wrong, because exceptions are a failed experiment and will never work or reach a consensus. Move past it.

I really, REALLY don't understand what's so wrong with just.. making methods return what they say they do, as plain objects. The antagonism towards doing so is literally just ingrained culture with zero sensible justification.


> I really, REALLY don't understand what's so wrong with just.. making methods return what they say they do, as plain objects. The antagonism towards doing so is literally just ingrained culture with zero sensible justification.

From experience with Rust, a major concern is lack of stack traces in error messages. Rust has libraries that built up around Result to address this (like Anyhow), but by the time you've included Anyhow with stack traces enabled you've basically reinvented checked exceptions in Result types. For Rust this is probably more ergonomic than introducing exceptions into the language, but Java already has checked exceptions that work like Anyhow does out of the box.

That's the part that you seem to be missing: checked exceptions are nearly identical semantically to result types, and they already exist and are widely used in the language. There are a few minor ergonomic problems that discourage their wider use, but it makes way more sense for Java to fix those than for people to migrate to Result types, which are not nearly as ergonomic in Java as they are in Rust or similar.

You tried to address this upthread, but it was frankly a pretty bad strawman that contrasted a horrible example of checked exceptions with an idealized example of a Result type. You then compounded the strawman with a double standard by demanding that the checked exceptions example immediately account for what would happen in the catch block while hand-waving away the handling of Left values in the Result type as something that we can deal with at some unspecified later time.

Dealing with failures later at the edges of the system is the reason why exceptions were invented, so it's highly disingenuous to claim that as a capability unique to Result types.


> by the time you've included Anyhow with stack traces enabled you've basically reinvented checked exceptions in Result types

If the error is wrapped into an `anyhow::Error`, and the result in `anyhow::Result<T>`, it has less information than checked exceptions. Coz the type now only tells you the call can fail, but not how (it's anyhow!).

> That's the part that you seem to be missing: checked exceptions are nearly identical semantically to result types

That's the crux.


> it doesn't require weird special syntax

But to use that `Either` object ergonomically languages do add extra syntax. Rust with the `?` and Scala with the for-comprehension.

The interactions with HOFs are also subtle.

In Rust it requires a smart use of type params for `collect`.

In Scala (following the Haskell tradition) there's the pair of coloured functions `traverse` and `map`. But `traverse` is only available in third-party functional programming libraries (Cats or Scalaz). So out of the box there isn't a way to cleanly `map` a function that returns the `Either` type.

> now we're catching ALL exceptions, yay

Your example is not that different from upcasting.

    val foo: Either<Exception, Foo> = // Either is covariant
      myClass.getFoo() // returns Either<MyError, Foo>


the difference is that in catch (Error e), you're actually catching the Error type, the superclass of exception

in Either<Error, Foo>, Error is not that, it's usually a sealed class with the full range of things that can go wrong with that thing, no more, no less, eg

    sealed class ChangePasswordErrors {
        class NotAuthenticated() : ChangePasswordErrors()
        class ChangePasswordError() : ChangePasswordErrors()
        class ReusedPassword() : ChangePasswordErrors()
    }
or whatever (but I could have made that clearer)

and no, languages should not add weird syntax to deal with Either, Option, Try etc. Just call map, flatMap, fold.


> the Error type, the superclass of exception

> in Either<Error, Foo>, Error is not that

This is confusing. On the JVM `Error` and `Exception` are both subclasses of `Throwable`. If you meant a specific error type, give it another name. (I used `MyError` in the comments above.)

> sealed class ChangePasswordErrors {

If some kind of failure has to be handled, In Kotlin you can write:

    sealed class ChangePasswordResponse {
        class Success(): ChangePasswordResponse()
        class NotAuthenticated() : ChangePasswordResponse()
        ...
> Just call map, flatMap, fold.

Enjoy your callback hell.


Good poit re using MyError, that is indeed what I meant in the Either case

Not sure what you mean by callback hell, I've never experienced that using Either. I just pass the Either from the db layer or whatever to the resource layer or whatever, where I fold it into a 200 with the requested thing or a 4xx or 5xx or whatever depending on the MyError (which is usually a sealed class that lists everything that can go wrong with the thing)


Odersky actually hates Either and refuses to use it in Scala for errors.


I find this extremely weird.

Why even use Scala then? Is it just for the implicits and long build times?


> or put throws everywhere

Or put Result/Error etc. type everywhere. It’s literally analogous.


it's literally not, I'm sorry but folks who think this (and it's extremely common) simply haven't familiarized themselves with the differences


Repeating the same stuff won’t make it true.


Going off-topic, I've been on both sides of seeing the analogousness or not. I think there might be two modes of thinking for deciding whether two things are analogous.

Mathy people inspect the "shape" of, and interactions within the two things. Other people go for intuitive vibes. For more concrete stuff the two modes of thinking usually match up.

Summation and multiplication are literally different things, but a lesson in group theory tells you (ℝ, +) and (ℝ⁺, ×) are isomorphic.


Something being analogous to something else means, that there is a bidirectional function/mapping between the two.

One can easily map any Result-typed expression to a checked exception one, and vice versa.


> Objectively better

> is forced to pattern match on every interaction with object now

Not sure if trolling.


Just because the languages en vogue right now use result tuples does not invalidate exception handling altogether, no matter how strongly you may prefer them. It’s tradeoffs all the way down.


What a shallow critique. Object oriented error handling (which is what returning objects instead of exceptions is) isn’t used because it’s trendy, it’s used because it’s objectively (badumtish) better.


You didn’t get the point, then. It isn’t objectively better, but some people argue it is. Having an opinion doesn’t necessarily mean it’s true.


Kotlins compiler built in null safety is objectively better than Java's lack of the same

people disagree about that too, it's just that they're wrong


Kotlin’s null-safety is better and exceptions are better than Result types.


The fact that exceptions are used is testament that they do have some utility. Same can be said of either/result.

‘Objectively better’ is appropriate either in very simple choice scenario, or in a case where one is throwing away ones integrity.


I don't see much distinction between the two because either way you have to change the signature of your function and add a bit of boilerplate at every call site to handle the error or pass it along. I think the only meaningful difference between result types and throwing semantics is that result types work with expressions, which are arguably more concise, while throwing semantics work with entire blocks, which can be more useful in some cases.


This means you will have to write an if after every method call which is annoying and makes code less readable (source: had to write lots of ifs several days ago while writing a C library. My code consists mostly of ifs with error handling, because almost every function - even encoding conversion - can fail. I think that if companies hired low-paid trainees just for the purpose of writing ifs they could save a lot).


I don't mean to be uncharitable but no, that's not how it works. You pass the Either<Error, Foo> or Maybe/Option<Foo> around, usually to the edge of the system where you map, flatMap or fold it into something.


But this is ugly, for example, if I have a square root function, I don't want it to accept Error.


No function should accept error


Presumably one of these applies:

* You have a sequence of steps, and only care about a general error condition occurring somewhere in range of those; in which case, it makes sense to abstract something to treat those steps as a unit and collect any error from that

* You don’t need to handle any errors directly, and wish to propagate them to the caller; in which case, doing so explicitly is better than not

* You should handle the errors; in which case, complaining about that doesn’t make much difference


If it's repetition, you can abstract it away.


There are very few cases where you can abstract away error handling because it's extremely context dependent.


I tend to agree with you, not for the different semantic aspect, since they both are capable of expressing the same thing (with checked exceptions), but for the performance impact of exceptions. If I'm not mistaken throwing an exception has a much larger cost than a simple return due to needing to construct the stacktrace.

So using exceptions for an expected path of a function seems sub-optimal.


Generally speaking the Left (error) value of a Result is unexpected during normal operation. You "expect" it in that you prepared for a specific kind of failure but you won't generally see a whole bunch of Left values in a hot loop unless something has gone horribly wrong, in which case performance is not your primary concern.

Meanwhile, one of the biggest weaknesses of Result types is precisely that they don't carry a stack trace with them, which can make it hard to debug a failure when one occurs. Libraries like anyhow in Rust [0] actually go out of their way to retrofit Error types with stack traces because it turns out that most of the time we actually want them.

[0] https://docs.rs/anyhow/latest/anyhow/


I don't see the difference. Exceptions are error handling and with this change the language will be better equipped to handle them.


Exception based error handling is so bad and unsafe that adopting functional error handling with Either, Try etc as implemented by functional addon libraries for many languages, while not yet common, in time it will become the new default even in OO languages. (just like it's been the default in functional languages for decades)

Functional error handling types are much simpler, safer and more powerful.

Simpler because they don't rely on dedicated syntax- they're just regular objects no different to any other object.

Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more. (no risk of ignoring errors and no risk of catching a higher level of error than desired, ubiquitous bugs in exception based error handling)

Powerful because they support map, flatmap, applicative etc, making it easy to eg chain multiple computations together in desired ways, which is unwieldy and bug prone when using exceptions.

> What is wrong about dedicated syntax

It adds complexity to the language! It could be that, when learning Java, Kotlin and any other language, we learn that methods return what they say they do... and that's that. No weird dedicated syntax and magic, special treatment for returning anything other than the happy path, and the HUGE complexity that comes with it, eg the dedicated syntax itself and how it behaves, differences between checked and unchecked exceptions, hierarchies of exceptions etc etc.

> Exceptions are easier

That's the point, they're not. Exceptions based error handling is unnecessary, hugely complex, doesn't compose at all, obfuscates or straight up hides what can go wrong with any given call, so leads to countless trivially preventable bugs... I could go on. And after decades of use, there's still no consensus about what exceptions should be or how they should be used. Exceptions are a failed experiment and I have no doubt that in ten years, Java, Kotlin and many other languages will acknowledge as much and move away from it the same way Joda Time outcompeted and replaced the horrible Java date and time library.


> unlike exceptions, they force callers to handle all potential outcomes, but no more.

Checked exceptions in java also have that property.

> they support map, flatmap, applicative etc, making it easy to eg chain multiple computations together in desired ways, which is unwieldy and bug prone when using exceptions.

Isn't that exactly the kind of thing this proposal aims to improve upon?

> What is wrong about dedicated syntax [...] It adds complexity to the language!

It sounds like you're basically arguing that the less syntax a language supports, the easier it is to use. But poll some beginner programmers about which language they find easier to use between Lisp and Python and I'm certain they'll say Python.

Syntax has a purpose and if you take all the complexity out of the syntax then you'll just be pushing that complexity into the standard library instead. That's not inherently better, and personally I think it makes more sense to distribute that complexity across the various tools involved. That way, the syntax of the language can align better with the way we read and interpret natural language, and make it easier to process some of that complexity.


> Checked exceptions in java also have that property

No they don't. How many times have you seen code like

    try { //whatever } catch (Exception e) { //now we're catching ALL exceptions, yay }
or, worse

    try { //whatever } catch (Error e) { //lol }
re syntax, I guess some folks like more syntax but the point is if the syntax is being added to solve a problem that's solved in a way that's strictly objectively better in another way, it's pointless (and we're not talking about just complex syntax and all the foot guns and complexity that comes with hierarchies with exceptions, differences between checked and unchecked etc etc, we're talking about actual execution - try catch is at the end of the day a JVM hack that slows down execution and does something weird which could be easily avoided by just returning plain objects


And in your scenario where you handle errors with an Either type, how many times have you seen code where that kind of thing just gets upcasted because the caller can't be bothered to deal with the specialized error? I think this is a strawman argument against exceptions and it's equally possible to do it wrong with your way as it is with my way.


Rust:

   .unwrap() // error not possible
Kotlin:

   someNullValue!!.doIt() //can't be null here


> Exception based error handling is so bad and unsafe that adopting functional error handling with Either, Try etc as implemented by functional addon libraries for many languages, while not yet common, in time it will become the new default even in OO languages. (just like it's been the default in functional languages for decades)

I honestly doubt this. Especially since Java is investing further into exceptions with this JEP.

> Functional error handling types are much simpler, safer and more powerful. Simpler because they don't rely on dedicated syntax- they're just regular objects no different to any other object.

Who cares? Languages can build the syntax, make it easier, and make it consistent rather than having specific dialects that comes from libraries. Either, Try, Result which one do I choose? This is better delegated to the language rather than DSLs.

> Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more. (no risk of ignoring errors and no risk of catching a higher level of error than desired, ubiquitous bugs in exception based error handling)

> Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more. (no risk of ignoring errors and no risk of catching a higher level of error than desired, ubiquitous bugs in exception based error handling)

I'm not sure they are safer at all. Either, Try, and Result don't force you to check the errors, they all offer escape hatches. How many times has there been rust code that just calls `.unwrap` because the author doesn't care or thinks the error can't occur. I've seen tons of Kotlin code that uses !! to get out of null checking because the author doesn't believe something could be null and can't handle it actually being null.


I'm with you right up until the dedicated syntax point.

Failure modes aren't going away, (especially with our tendency towards slinging JSON at each other as fast as possible.)

So, make a good syntax for it! Then accounting for failure becomes pleasurable, so developers will do it. Take the hit and make your user base learn it once, early.

But perhaps I misunderstood what 'dedicated syntax' is. I want syntax for map/flatMap because it's really broadly useful. But I don't like it when one particular implementation gets special syntax, and the rest don't (looking at you, Rust-Result, and null-coalescing operators).

> Exceptions are a failed experiment They still need to exist in some form. We're not going to case-match over every plus and divide to deal with overflow and div0.

"Exceptions should only be used exceptionally" is wisdom that never really stood a chance, because mainstream languages didn't provide a way to handle non-exceptional errors.


Exceptions are great! Some years ago I wrote a whole essay defending them from this sort of argument:

https://blog.plan99.net/what-s-wrong-with-exceptions-nothing...


Mike do you have any more details on this:

> HotSpot will do things like recompile methods on the fly to disable stack trace generation, if profiling shows that your code is throwing and catching exceptions way more often than expected

I was under the impression that to get good performance here you had to override getStackTrace.



Thanks! Looks like the linked source file doesn’t exist on master. I’ll have to see if I can track it down when I have some more time.



I've seen codebases using Either and other monad constructions that worked fine on the happy path but had nowhere near adequate error handling, I think they thought because they used Either they didn't need to think about error handling.


It's still a net win.

I program very fast-and-loose in Haskell.

Sloppiness shows up a lot more visibly in the source code.

If I write:

    (200, user) <- getUser userId
I can see the tech debt right there on the page. I'll probably even get warnings. Even with with other variations like:

    getUser userId >>= \case
        (200, user) -> ...
        (404,    _) -> ...
        _           -> error "TODO/FIXME
This kind of thing tends to disappear in my Java day job:

    User user = getUser(userId);
What's wrong with the above? Maybe nothing, maybe everything. I don't know! I need to start ctrl-clicking into definitions to try to hunt down the failure modes.


Note catching exceptions too close to where they are thrown is as much of a risk as catching them too far away.

I have dealt with so much Scala code that is just too clever. Like the kind that tries to parallelize a trivial task but has race conditions anyway and doesn't use all the CPUs. You can spend two days trying to fix it or you can finish the job in 20 minutes with the ExecutorService.


> Note catching exceptions too close to where they are thrown is as much of a risk as catching them too far away.

Yes, another another reason to avoid them.


And to avoid having any code ever fail. It was a formative experience to me to type in the source of a terminal emulator in C for CP/M that I ported to Microware’s OS-9 out of a 1984 Byte magazine that tried to do things the Either way without language support and noted about 4/5 of the code was error handling interspersed with the happy path. Either is not much better than errno in the end.


Next: handling classes in functions.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: