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??
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.
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.
Kotlin does have interop with Java, but is limited by either the features not existing in Java (non-nullable types) or behave differently in Java (records, etc.).
You have to explicitly annotate that a Kotlin data class is a Java record due to the limitations Java has on records compared to data classes [1]. This is similar to adding nullable/not-null annotations in Java that are mapped to Kotlin's nullable/non-nullable types.
Where there is a clean 1-1 mapping and you are targeting the appropriate version of Java, the Kotlin compiler will emit the appropriate Java bytecode.
by default it'll be exposed as a `java.lang.Object` and they've thought about using compiler plugins to generate methods returning `Optional<>` or `Result<>` instead
thank you for the link to the talk! I actually witnessed the talk live, but must have completely missed this or simply forgot that this was answered :D
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).
The ship has sailed the moment Kotlin believed it could break away from the JVM ecosystem. And for a good reason, it would be just slowly consumed by the progress of Java.
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.
> 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.
Because Kotlin took a dumb turn about 5 years ago and decided that multiplatform was their future instead of the jvm (because they felt threatened by React Native and Flutter on Android). Point being, they don't want to tie themselves to the jvm anymore. The language has been stagnant for years as they've had to reimplement tons of java libraries and also shoehorn the extreme complications of multiplatform into the awful gradle build system. All for this dumb dream that has gone no where.
You can argue the tooling is still immature... but kmp really has been taking off since cmp has gone stable on ios. The kmp library ecosystem is even getting quite large. Also, kmp has very distinct advantages over React Native and Flutter, they didn't just copy-paste from RN or Flutter.
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.
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.
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.
Both Rust and Go are good examples of modern languages with no exceptions.
Not coincidentally, both provide a panic mechanism, which is intended for failures that are either unrecoverable or at least not locally recoverable. Both languages allow you to "catch" panics, but this mechanism is constrained and impractical to use for normal error handling. Instead, it's used to e.g. prevent a service from crashing if individual requests fail unrecoverably.
The point is that these languages both demonstrate a simple design for exception-free languages.
(Although Rust's approach is better in several ways, partly because it has a better type system.)
The innovation here is, I think, the use of union types. The problem with errors as standard algebraic data types (ADTs) is you end up with lots of boilerplate to transform from one ADT to another, as errors propagate through the system. With union types (as found in Typescript and Scala 3) you can add and remove types from the union in an ad-hoc manner. IIRC Elm doesn't have union types, so I think the blog post is a bit inaccurate.
21 comments
[ 4.8 ms ] story [ 59.3 ms ] threadBackground: 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??
From the proposal discussion[0], the runtime representation on the JVM will just be `Object`.
[0]: https://github.com/Kotlin/KEEP/discussions/447#discussioncom...
A `Result<T, E>` return type is way better.
This feels like it'll be viewed like Java's `Date` class: a mistake to be avoided.
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.
You have to explicitly annotate that a Kotlin data class is a Java record due to the limitations Java has on records compared to data classes [1]. This is similar to adding nullable/not-null annotations in Java that are mapped to Kotlin's nullable/non-nullable types.
Where there is a clean 1-1 mapping and you are targeting the appropriate version of Java, the Kotlin compiler will emit the appropriate Java bytecode.
[1] https://kotlinlang.org/docs/jvm-records.html#declare-records...
https://www.youtube.com/watch?v=IUrA3mDSWZQ&t=2626s
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).
C++ cannot get away from C, Typescript cannot get away from JavaScript, and so forth.
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.
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.
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?
Not coincidentally, both provide a panic mechanism, which is intended for failures that are either unrecoverable or at least not locally recoverable. Both languages allow you to "catch" panics, but this mechanism is constrained and impractical to use for normal error handling. Instead, it's used to e.g. prevent a service from crashing if individual requests fail unrecoverably.
The point is that these languages both demonstrate a simple design for exception-free languages.
(Although Rust's approach is better in several ways, partly because it has a better type system.)