Kotlin's Rich Errors: Native, Typed Errors Without Exceptions
26 points
5 days ago
| 7 comments
| cekrem.github.io
| HN
spankalee
4 minutes ago
[-]
Does anyone know of a great write up on exceptions vs union or either typed returns?

I'm building a new language, somewhat similar to TypeScript in some ways, and so far I have exceptions and try/catch expressions, but also Optional<T> and Result<T, E> types.

I'm familiar and used to exceptions, so I included them so at least near-fatal errors (ie, actually exceptional) could be caught at high levels in the stack. But I'm unsure if there's a strong argument that resonates with me yet that the language shouldn't have exceptions at all. Arguments that exceptions are untyped can be solved with things like checked exceptions, and I do find Go-style code to be quite verbose.

What's the best current reading on this?

reply
armchairhacker
2 hours ago
[-]
These remind me of checked exceptions in Java. Ironically, Kotlin removed checked exceptions because they tend to be annoying more than useful: there's no clear guideline to whether an exception is checked or unchecked, some functions like IO and reflection have them while others don't, they're verbose especially when closures are involved, and lots of functions simply catch and rethrow checked exceptions in unchecked exceptions.

Which leads me to: why is Kotlin implementing this in a non-JVM compatible way, instead of introducing checked exceptions with better language support? All the problems stated above can be avoided while keeping the core idea of checked exceptions, which seems to be the same as this proposal.

From the GitHub discussion, I see this comment (https://github.com/Kotlin/KEEP/discussions/447#discussioncom...):

> The difference between checked exceptions from java and error unions in this proposal is how they are treated. Checked exceptions are exceptions and always interrupt the flow of execution. On the other hand, errors in this proposal are values and can be passed around as values or intentionally ignored or even aggregated enabling the ability to use them in async and awaitAll etc.

But is this a real difference or something that can be emulated mostly syntactically and no deeper than Kotlin's other features (e.g. nullables, getters and setters)? Checked exceptions are also values, and errors can be caught (then ignored or aggregated) but usually interrupt the flow of execution and get propagated like exceptions.

reply
loglog
1 hour ago
[-]
The plain Java equivalent of the proposed semantics would be a type system extension similar to JSpecify: @Result(ok=Ok.class, error={Checked1.class, Error2.class}) Object function() combined with enough restrictions on the usages of results of such methods (e.g., only allow consuming the results in an instanceof pattern matcher; not even switch would work due to impossibility of exhaustiveness checking).

The one feature that the proposed Kotlin error types share with Java checked exceptions is that they can be collected in unions. However, the union feature for checked exceptions is pretty much useless without the ability to define higher order functions that are generic over such unions, which is why checked exceptions fell out of favor with the spread of functional APIs in Java 8.

reply
ajrouvoet
7 minutes ago
[-]
This last point is the key observation.
reply
Tyr42
17 minutes ago
[-]
I think bridging this syntax onto legacy checked exceptions from java would make a lot of sense.
reply
sirwhinesalot
2 hours ago
[-]
Exceptions are cheap on the happy path and super expensive on the error path.

Checked exceptions only make sense for errors that are relatively common (i.e., they aren't really exceptional), which calls for a different implementation entirely where both the happy path and the error path have around the same cost.

This is what modern languages like Rust and Go do as well (and I think Swift as well though don't quote me on that) where only actually exceptional situations (like accessing an array out of bounds) trigger stack unwinding. Rust and Go call these panics but they are implemented like exceptions.

Other errors are just values. They have no special treatment besides syntax sugar. They are a return value like any other and have the same cost. As they aren't exceptional (you need to check them for a reason), it makes no sense to use the exception handling mechanism for them which has massively skewed costs.

reply
HendrikHensen
36 minutes ago
[-]
> Rust and Go call these panics but they are implemented like exceptions.

I don't know about Rust, but a very important difference between Java exceptions and Go panics, is that a Go panic kills the entirely process (unless recovered), whereas a Java exception only terminates the thread (unless caught).

It's a little off-topic, but I wanted to clarify that for passer-bys who might not know.

reply
tester756
7 minutes ago
[-]
>Exceptions are cheap on the happy path and super expensive on the error path.

Depends on the software, huh.

reply
jpalepu33
1 hour ago
[-]
The discussion around checked vs unchecked exceptions always comes down to ergonomics vs safety.

Having worked extensively with Node.js (callback hell, then Promises), I appreciate how error-as-value patterns force you to think about failure cases at every step. But the reality is most developers don't - they either:

1. Ignore the error case entirely (leading to silent failures) 2. Bubble everything up with generic error handling 3. Write defensive code that becomes unreadable

Rust's Result<T, E> with the ? operator found a sweet spot - you have to acknowledge errors exist, but the syntax doesn't make it painful. The key innovation is making the happy path concise while forcing acknowledgment of errors.

For Kotlin specifically, I'm curious how this interops with existing Java libraries that throw exceptions. That's always the challenge with these proposals - they work great in greenfield code but break down at library boundaries.

The real question: does this make developers write better error handling code, or just more verbose code? I'm cautiously optimistic.

reply
hackthemack
6 minutes ago
[-]
Similar thoughts.

One thing I notice in enterprise java software that I have to reed through and update, is that too many times, every developer just wraps everything in an exception. I do not have vast insight into all java code, everywhere, but in my little corner of the world, it sure looks like laziness when I have to dig through some ancient java code base.

reply
HendrikHensen
29 minutes ago
[-]
The biggest problem is that people treat it as a dichotomy: either exceptions or error values. But that's a false dichotomy.

There would be real value in a language which would have both.

Error values are perfect for un-exceptional errors, e.g. some states of a business logic. The name that the user entered is invalid, some record is missing from the database, the user's country is not supported. Cases that are part of the business domain and that _must_ be handled, and therefore explicitly modeled.

Then there is the grey area of errors that one might expect (so not truly exceptional) but are not related to the business logic. These could be for example network timeouts, unexpected HTTP errors (like 503), etc. For those, there is often no explicit handling in the domain that makes sense. So it's convenient to just throw an exception, let it automatically "bubble" to the highest level (e.g. the HTTP controller) and just return some generic error (such as HTTP 500).

There are also truly exceptional cases, that you really shouldn't encounter in your program, such as null-dereferences, invalid array index access, division by zero, etc. These indicate a bug in the code (and might be introduced explicitly with assert-style checks). The program is in an unknown, compromised state, so there's really nothing left to do than throw an exception or panic. An error value makes very little sense in this case.

I often have the discussion with friends, why a division operator, or an array access, doesn't return a `Result` type in nice languages such as Rust? Surely, if they care about error values, then each operation that can fail, must return a `Result` rather than panic (throw an exception). It is an interesting through experiment at least.

reply
Tyr42
18 minutes ago
[-]
Sounds like checked and unchecked exceptions.

I mean, this could be a syntax wrapper for java checked exceptions right?

Those are isomorphic to Result<_, Err> in that you must handle or propagate the error. The syntax is different, sure.

reply
ixtli
1 hour ago
[-]
This is nice, and I develop often in Kotlin, but none of this will really achieve what people want so long as any line can possibly throw a runtime exception.
reply
loglog
47 minutes ago
[-]
I think that the problems with unchecked exceptions are due to the simultaneous presence of both checked and unchecked exceptions. The designers must have thought that checked exceptions would be the rule, but left an escape hatch.

If there were no checked exceptions to begin with, people might have thought about making the Java compiler (and later language server) infer all possible exception types in a method for us (as effect systems do). One could then have static analysis tools checking that only certain exception types escape a method, giving back checked exceptions without the type and syntax level bifurcation.

On the other hand, if all exceptions were checked, they would inevitably have had to implement generic checked exception types, ironically leading to the same outcome.

reply
esafak
1 hour ago
[-]
Slated for release in 2.4 this summer. Track in https://youtrack.jetbrains.com/issue/KT-68296
reply
cekrem
4 days ago
[-]
The Java interop compromise is probably the biggest weakness of the proposal - it works beautifully within Kotlin but degrades at boundaries. This is similar to how Kotlin's nullable types (String?) become platform types in Java.

I think it's good nonetheless to add stuff to Kotlin that won't translate 1:1 to Java, both because Java is evolving but also because Kotlin is used in "Native" (non-JVM) contexts as well (not extensively, but hopefully that'll change).

reply
pjmlp
2 hours ago
[-]
That is always the gotcha with guest languages.

C++ cannot get away from C, Typescript cannot get away from JavaScript, and so forth.

reply
simon_void
4 days ago
[-]
Rich Errors look promising to me, but what about interop with Java? What will the return-type of a function be on the Java side be, if the return type on Kotlin side is: Int | ParseError | SomeOtherError?

Background: unions aren't restricted to one normal type and one error type, but to one normal type and any number of error types, so this can't be modelled as syntactic sugar on top of an implicit Either/Result type, can it??

reply
pjmlp
2 hours ago
[-]
Kotlin folks seem to mostly care about Java as bootstrap to their own ecosystem.

The anti-Java bias, against the platform that made it possible in first place and got JetBrains a business, is quite strong on Android, fostered by the team own attitude usually using legacy Java samples vs Kotlin.

reply
pianoben
3 hours ago
[-]
> what about interop with Java?

From the proposal discussion[0], the runtime representation on the JVM will just be `Object`.

[0]: https://github.com/Kotlin/KEEP/discussions/447#discussioncom...

reply
dingi
37 minutes ago
[-]
They only care about Java -> Kotlin integration. Not the other way around. It has been like this for a long time. Looks like an extractive relationship to me to be frank.
reply