https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/...
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.
A b() throws C;
And A b()(using Throws[C]);
are exactly the same.Exceptions are nothing like gotos/long-jumps, it’s just “pressing escape key once vs keeping it pressed”, basically.
- 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.
Why did you link to Kotlin? It doesn't appear to have `throws MyError`.
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.
vs just plainly indicating you will get Either<Error, Foo>
this has nothing to do with sealed classes
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 etcNotably, 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)
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 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 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.
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.
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...
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.
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.
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.
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>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.
> 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.
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)
Why even use Scala then? Is it just for the implicits and long build times?
Or put Result/Error etc. type everywhere. It’s literally 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.
One can easily map any Result-typed expression to a checked exception one, and vice versa.
> is forced to pattern match on every interaction with object now
Not sure if trolling.
people disagree about that too, it's just that they're wrong
‘Objectively better’ is appropriate either in very simple choice scenario, or in a case where one is throwing away ones integrity.
* 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
So using exceptions for an expected path of a function seems sub-optimal.
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.
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.
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.
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 .unwrap() // error not possible
Kotlin: someNullValue!!.doIt() //can't be null hereI 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.
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.
https://blog.plan99.net/what-s-wrong-with-exceptions-nothing...
> 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.
https://github.com/openjdk/jdk/blob/6d5699617ff0985104a8bb5f...
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.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.
Yes, another another reason to avoid them.