5 ms·
Kotlin's rich errors: Native, typed errors without exceptions
- simon_void 9mo agoRich 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??
- pianoben 9mo 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#discussioncomment-14230662 https://github.com/Kotlin/KEEP/discussions/447#discussioncom...
- ubertaco 9mo agoOof. That's pretty gross: just throw away all typesafety? 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.
- pjmlp 9mo agoKotlin 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.
- pianoben 9mo agoAndroid folks have good reason to have anti-Java bias. Their bias, as it happens, is against old Java, which they are constrained to use as fallout from the Oracle lawsuits of yore. Kotlin breathed new life into Android in a meaningful way. On backend teams, I've not personally encountered much anti-JVM bias - people seem to love the platform, but not necessarily the language. (yes I know there's desugaring that brings a little bit of contemporary Java to Android by compiling new constructs into older bytecode, but it's piecemeal and not a general solution)
- pjmlp 9mo agoLies, damm lies. They cherry pick whatever they feel like from OpenJDK. And even though Oracle was right, given that Android is Google's J++, in this case they had better luck than Microsoft. They don't take more from OpenJDK because then their anti-Java narrative doesn't work out. But there is some schadenfreund, to keep Kotlin compatibility story relevant they are nonetheless obligated to keep up with is mostly used on Maven Central, thus the updates up to Java 17 subset.
- pianoben 9mo agoMaybe I'm wrong about the state of Java in Android today - it's been a few years since I did that work full-time. But I do remember when Kotlin broke on to the scene in 2015, and most of us were thrilled to finally move beyond Java 7! The embrace of a non-Java language was grassroots and genuine; Google's adoption came several years later. J++ though, now that is a blast from the past! I think I still have a J# book from my student days, somewhere :)
- andrekandre 9mo ago> But I do remember when Kotlin broke on to the scene in 2015, and most of us were thrilled to finally move beyond Java 7! n=1 but i was there with android studio v0.01 (or thereabouts) using kotlin for a production app cause i was so sick of old-java + eclipse... google was asleep at the wheele imo and android development would be nowhere near where it is today without jetbrains
- pjmlp 9mo agoCompared to Apple and Microsoft, Android development is mostly outsourced. None of the development environments is from Google, none of the languages as well, or the build tools for app developers (Internally they use Bazel and Soong). Naturally having gone into bed with JetBrains for the IDE, after leaving NDK users without IDE tooling for almost two years during the IDE transition, the deal was in place to push Kotlin as well. I am surprised Google hasn't yet bought JetBrains.
- tadfisher 9mo agoThis is needlessly divisive. JetBrains does not owe two-way interop to the Java ecosystem. There are many Kotlin features that do not have clean interop with Java; Compose, coroutines, and value classes come to mind. And it turns out that this mostly benefits Java, because these features are not built with the kind of engineering rigor that Java language features enjoy, and some of these features would behave way better with support in the VM anyway. Where it makes sense, they are already moving closer to Java/JVM-native feature implementations; for example, data classes already have two-way support via records, and value classes are almost there (waiting on Valhalla GA). Besides, wouldn't you want this stuff represented in the Java type system anyway? Otherwise you get the Lombok problem, where you have this build dependency that refuses to go away and becomes a de facto part of the language. Result<T, E> is not quite the same as rich errors which explicitly are not representable by user types.
- pjmlp 9mo agoI know it is divisive, yet many Kotlin folks don't appreciate the platform that makes their ecosystem possible in first place. From all the JVM guest languages, possibly fostered by Android team, they are the most anti-Java ecosystem. Clojure folks appreciate being a hosted language, Scala is kind of they would rather have Haskell but still JVM is kind of cool, Groovy was usually a way to script applications. Kotlin, well those behave as if ever the JVM would be one day rewriten in Kotlin, they have Android for that. The features you mention will never be supported in Android by the way, at least I don't believe Google will ever bother to do so.
- rhdunn 9mo agoI view the relationship between Kotlin and Java like that between C++ and C. The two-way interop is one of Kotlin's advantages as it makes porting code from Java to Kotlin easier, or using existing Java libraries. For example, you don't have/need something like Scala's `asJava` and `asScala` mappers as the language/standard library does that mapping for you. The interop isn't always perfect or clean due to the differences in the languages. But that's similar to writing virtual function tables in C -- you can do it, and have interop between C and C++ (such as with COM) but you often end up exposing internal details.
- dingi 9mo agoThey 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.
- tadfisher 9mo agoAnyone who is writing Kotlin libraries to be consumed by Java code is going to either avoid this feature or write wrapper functions for better Java interop. There is no reason to accuse language designers of lock-in by designing features that don't have a clear equivalent on every possible foreign interop target.
- rhdunn 9mo agoKotlin 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. [1] https://kotlinlang.org/docs/jvm-records.html#declare-records-in-kotlin https://kotlinlang.org/docs/jvm-records.html#declare-records...
- rileymichael 9mo agoby 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 https://www.youtube.com/watch?v=IUrA3mDSWZQ&t=2626s https://www.youtube.com/watch?v=IUrA3mDSWZQ&t=2626s
- simon_void 9mo agothank 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
- cekrem 9mo agoThe 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).
- pjmlp 9mo agoThat is always the gotcha with guest languages. C++ cannot get away from C, Typescript cannot get away from JavaScript, and so forth.
- zaphirplane 9mo agoThat is more interoperability than guest language situation. I don’t think c++ is a c guest. Ts has a different approach to the js self.
- pjmlp 9mo agoC++ started as a C macro preprocessor, designed to work as C in UNIX, and by CFront 2.0 there was already too much compatibility to throw away. Ts is basically a linter, everything else is JS.
- zaphirplane 9mo agoc++ bootstrapping as a transpiler back when it was a 1 man show is hardly where it’s at today. The point about self handling in ts should demonstrate there is more than just strip the types going on
- pjmlp 9mo agoThere is the whole fit into the same UNIX compiler toolchain as C, without additional changes to the linker, include files, object and archive files.
- armchairhacker 9mo agoThese 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#discussioncomment-14224544 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.
- sirwhinesalot 9mo agoExceptions 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.
- esafak 9mo agoSlated for release in 2.4 this summer. Track in https://youtrack.jetbrains.com/issue/KT-68296 https://youtrack.jetbrains.com/issue/KT-68296
- ixtli 9mo agoThis 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.
- loglog 9mo agoI 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.
- jessicest 9mo ago[dead]
- jpalepu33 9mo agoThe 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.
- HendrikHensen 9mo agoThe 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.
- spankalee 9mo agoDoes 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?
- hutao 9mo agoOne of the most thorough articles on error handling in programming language design that I've read is this one: https://joeduffyblog.com/2016/02/07/the-error-model/ https://joeduffyblog.com/2016/02/07/the-error-model/. It was written by Joe Duffy, who worked on Microsoft's experimental Midori language. Another relevant article is Robert Nystrom's "What Color is Your Function?": https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ https://journal.stuffwithstuff.com/2015/02/01/what-color-is-... This article is about async/await, but the same principles apply to error handling. This article uses colors as an analogy, but is really about monads. Both IO and exceptions can be denoted as a monad. What this means is that a function inside the programming language, A -> B, can actually be denoted by a mathematical function of the signature [[A]] -> M [[B]], for some monad M. For example, if we are dealing with the exception monad, M would be _ + Exception. A language such as Java implicitly executes in the IO + exception monad. However, the monad can also be exposed to the programmer as an ordinary data type, which is what Haskell does. When people talk about the tradeoff of exceptions versus Result<T, E>, or the tradeoff between preemptive concurrency and async/await, they are really talking about the tradeoff between making the monad implicit or explicit. (A language where all functions may throw is like one where all functions implicitly return Result<T, E>. A language where all functions may be preempted is like one where all functions are implicitly async, and all function calls are implicitly await points.) The theoretical technique of using monads to model the implicit effects of a programming language was pioneered by Eugenio Moggi, and the idea of making them explicit to the programmer was pioneered by Philip Wadler. Something else to think about is how monads stack. For example, how would you handle functions that are both async/await and throw exceptions? Does the answer change when the monad is implicit (e.g. throwing exceptions) or explicit (e.g. returning a result)?
- noelwelsh 9mo agoThe 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.
- brabel 9mo agoOf course Elm has union types, it’s like one of its main features! https://guide.elm-lang.org/types/custom_types.html https://guide.elm-lang.org/types/custom_types.html
- noelwelsh 9mo agoI mean union types in the programming language theory sense, not in the "we called this language feature union types" sense. Elm's union types are algebraic data types (ADTs). Union types in the PLT sense are what Typescript has. The main difference is that an ADT must be declared upfront, whilst a union type can be constructed in an ad-hoc manner based on whatever types are used together at a particular point in the program.
- brabel 9mo agoI used to think the same but after having many discussions just like this I accepted that there’s no agreed upon formal definition for what is a union type and what isn’t. Being created ad hoc or not seems like a made up on the spot distinction. If you do think there is a definition that most researchers agree on that’s distinct from ADT, do provide some sources.
- noelwelsh 9mo agoHere's Types and Programming Languages on union types. My take away is that the preferred terminology is union types for non-disjoint unions and sum types for disjoint unions. "Sum and variant types are sometimes called disjoint unions. The type T1+T2 is a “union” of T1 and T2 in the sense that its elements include all the elements from T1 and T2. This union is disjoint because the sets of elements of T1 or T2 are tagged with inl or inr,respectively, before they are combined, so that it is always clear whether a given element of the union comes from T1 or T2. The phrase union type is also used to refer to untagged (non-disjoint) union types, described in §15.7." p142 "The dual notion of union types, T1 ∨ T2, also turns out to be quite useful. Unlike sum and variant types (which, confusingly, are sometimes also called “unions”), T1 ∨T2 denotes the ordinary union of the set of values belonging to T1 and the set of values belonging to T2, with no added tag to identify the origin of a given element." p207