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.
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.
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.
Exceptions are cheap. Stack trace collection is the expensive part; if you make use of checked exceptions for domain errors you can just turn that off if you hit a performance problem.
Result or Error types may just be normal values, but they add overhead to the code as well when they’re ubiquitous.
Once they’re the standard error method then case every function has to intertwine branching for errors paths vs normal paths. Often the compiler has to generate unique code or functions to handle each instance of the Result type. Both add to code size and branching size, etc.
> 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.
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.
> 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).
It’s because JB intends to make money on their investment, unlike the other two. This is the reason why Kotlin ecosystem is a half baked mess – JB tries to artificially replicate all successful ecosystems without having a community.
How so? The Android ecosystem literally became the Kotlin ecosystem it was so successful. The multiplatform stuff came out of organic demand for re-using code at first between JVM backend and browser (Kotlin/JS), and then for re-using code between JVM, JS, Android and sharing the business logic on iOS, and then finally for being able to reuse UI code on iOS too for cases where native Swift UI can't be justified (obscure apps, etc).
Seems to me like Kotlin Multiplatform has been quite successful actually. The weakest part is Kotlin/Native where they could have just used GraalVM Native Image but I talked to the Kotlin team about that at the time and it was more of an opportunistic move than anything else - a compiler team in St Petersburg had been let go by Intel and JB could acquire the whole team cheaply.
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.
I 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.
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.
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.
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.
c++ 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
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.
This 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.
I 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.
I 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.
Android 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)
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.
Maybe 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 :)
ART is updatable via PlayStore since Android 12, however in 2026 the latest is a Java 17 subset, while the latest LTS is Java 25.
Kotlin only worked properly on Android after some folks pushed it from inside, and then they used Java 6 vs Kotlin samples to advocate for it.
In 2015 the latest Java version was 8, which never was properly supported on Android, the community had to come up with RetroLambda, before Google created desugaring support, think Babel but for Java.
Naturally it also meant that the performance of Java 8 features wasn't the same, e.g. lambdas make use of invokedynamic on the JVM, on Android they used to be rewriten into nested classes.
Even today, although Android documentation has Java and Kotlin tabs for code snippets, the Java ones are hardly taking advantage of modern features.
Naturally who learns Java on Android gets an adulterated view on the matter.
> 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
Compared 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.
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
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.
Anyone 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.
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.
The problem of mixing paradigms is that get confusing. Ideally all is represented equally (ie: All errors are `Result`) but is the handling that get confusing. Each option is a totally different control flow.
And it not compose (even if you use effects ) (and I mean ergonomically*) so you need to pick wich one to make first class
P.D: I'm pretty certain about the "not compose, in practice", I have seen lots of options and none looks nice, but open to corrections!
P.D.2: It should also consider the things on the dlang handling, and the midori article...
This means you can have a computation inside Either e monad (notice missing result type) which can occasionally produce an exception, and these exceptions are checked (Either String cannot produce SNAFU-type exceptions, only textual descriptions of what was wrong).
So, if you are developing your language, please consider embedding it in Haskell first. That would allow you to experiment with different type representations, at the very least: Result <e, t> of yours is a Result (e, t) in Haskell, which is very distinct from Result e t (Either e t).
I'm targeting only WASM at the moment. I wish I could get into Haskell, but I just can't read it. I really can't parse what's going on with types like `Result e t (Either e t)`.
In (not only) my opinion, Haskell's greatness is in "what is a language feature in most languages is a library in Haskell." One can model type system of a language by embedding it as a library into a Haskell and then develop it further as a standalone language if one prefers such path.
I did that embed-as-library thing several times. It was of great help, especially in the embedded systems and hardware circuits domains.
The `Either a b` type in Haskell is equivalent to the `Result<T, Error>` type in other languages. The only difference is in the naming: "Result" semantically implies error handling, while "Either" implies a more general usage as the alternative between two options. Either and Result are the binary sum type (the disjoint union between two types).
Contrast the definition of the Result type in Swift:
public enum Result<Success: ~Copyable & ~Escapable, Failure: Error> {
/// A success, storing a `Success` value.
case success(Success)
/// A failure, storing a `Failure` value.
case failure(Failure)
}
I'm not sure what the parent commenter meant when they claimed that "Result <e, t> of yours is a Result (e, t) in Haskell." In Haskell, `(e, t)` would be the pair type (the binary product type).
> The `Either a b` type in Haskell is equivalent to the `Result<T, Error>` type in other languages. The only difference is in the naming:
This is false and misleading.
The Result <E, T> usually (C#, at the very least, most probably C++ and many other languages) should always be fully instantiated. One usually cannot construct a type "function" like Result <E,> that needs a single type argument to instantiate a full type. The partial application on type level is not there in most languages, including Rust (a result of a little googling).
The Haskell's Either type can be instantiated to Be a two-type-arguments function, one type argument function and, finally, a fully instantiated type like Either String Int.
This means that Result <E, T> type effectively has a single type argument, namely pair of types. The Either type has two type arguments and can be partially applied.
You are right, in Haskell type constructors may be partially applied. In my opinion, this feature has less to do with any fundamental difference between `Either` in Haskell and `Result` in other languages, and more to do with Haskell's more powerful type system. In the same way, the pair type (a, b) in Haskell is also different from the pair types in other languages. This feature is called "higher-kinded types."
In particular, higher-kinded types are necessary to abstract over functors (or functions from types to types, * -> *). The list type constructor is a functor, and the partially applied type constructor `Either a` is also a functor. However, in languages without higher-kinded types, type variables can only be "ground types" (of kind *).
I don't agree with this statement:
> This means that Result <E, T> type effectively has a single type argument, namely pair of types. The Either type has two type arguments and can be partially applied.
The Result<T, E> type still takes two type arguments. The main distinction, in my view, is that Haskell allows types to be "higher-order." In fact, to be really pedantic, you could argue that the `Either` type in Haskell really takes one type argument, and then returns a function from types to types (currying).
This is kind of like the type-level equivalent to how many programming languages support some notion of function or procedure (and functions may have multiple arguments), but only more modern languages support higher-order functions, or allow variables to be functions.
One 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/. 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-... 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)?
That Midori article looks great, I'll give that a closer read. I actually used to work with Bob, and am familiar with the (wonderful!) function color article.
I think my biggest question might be addressed in the Midori article: with things like bounds checks and checked casts you already have exceptions (or panics), so should you have a way to capture them anywhere on the stack? Are they recoverable in some programs? So should you have try/catch even if you try to make most errors return values?
Another set of questions I have is around reified stacks. Once you have features like generators and async functions, and can switch stacks around, you're most of the way to resumable exceptions. I don't yet fully grok how code as the resume site is supposed to deal with a resume, but maybe resumable exceptions are a reason to keep them.
I'd never heard of "resumable exceptions" before, so I searched them up [1][2]. Is this another name for the language feature called "effect handlers" in OCaml 5?
Pretty much every language has a form of resumable exception known as a "function call". It's hard for me to understand why no one in the algebraic effects/effect handlers community has noticed this yet.
This is the difference between functions and effect handlers, to my understanding:
Functions map inputs to outputs, with a type signature that looks like A -> B. Functions may be composed, so if you have f: A -> B and g: B -> C, you have gf: A -> C. Function composition corresponds with how "ordinary" programming is done by nesting expressions, like g(f(x)).
Sometimes, the function returns something like Option<B> or Future<B>. "Ordinary" function composition would expect the subsequent function's input type to be Future<B>, but frequently you need that input to have type B. Therefore, optionals or futures require "Kleisli composition," where given f: A -> Future<B> and g: B -> Future<C>, you have gf: A -> Future<C>. Kleisli composition corresponds with "monadic" programming, with "callback hell" or some syntactic sugar for it, like:
let y = await f(x);
g(y)
Effect handlers allow you to express the latter, "monadic" code, in the former, "direct style" of ordinary function calls.
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 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.
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.
> why a division operator, or an array access, doesn't return a `Result` type in nice languages such as Rust?
Rust has standard library functions to do this, if you want. arr.get(index) returns an Option. Integer types have .checked_div for panic-free divide. Float already doesn’t panic on an invalid division - it just returns NaN or Infinity.
That's great! Though, by making it not forced and giving users a choice, you never know which library code you call might not use those features and still panic when you use it.
(Just to be clear, I don't really propose that a language should offer only panic-free operations; I just think it's a nice thought experiment and discussion to have).
It's absolutely a true dichotomy. If unchecked exceptions exist, all code must be carefully written to be exception-safe, and the compiler is not going to help you at all.
Of course it's convenient to be able to ignore error paths when you're writing code. It's also a lot less convenient when those error paths cause unexpected runtime failures and data corruption in production.
A preference for unchecked exceptions is one of my most basic litmus tests for whether a developer prioritizes thinking deeply about invariants and fully modeling system behavior. Those that don't, write buggy code.
Correct. Although the performance characteristics are different. An exception in Java generates a stack trace, which is relatively expensive. So not a great idea in un-exceptional code paths that need to perform well.
Stack trace collection is optional. If you actually hit a performance problem on a checked exception path you just override the stack trace collection not to happen. I do this in most of my projects that use checked exceptions as domain errors. Only once you have to panic, i.e wrap in an unchecked exception, stack trace collection happens.
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.
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