[HN Gopher] Kotlin's Rich Errors: Native, Typed Errors Without E...
       ___________________________________________________________________
        
       Kotlin's Rich Errors: Native, Typed Errors Without Exceptions
        
       Author : todsacerdoti
       Score  : 31 points
       Date   : 2026-01-18 12:45 UTC (5 days ago)
        
 (HTM) web link (cekrem.github.io)
 (TXT) w3m dump (cekrem.github.io)
        
       | simon_void wrote:
       | 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??
        
         | pianoben wrote:
         | > 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...
        
         | pjmlp wrote:
         | 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.
        
           | pianoben wrote:
           | 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)
        
             | pjmlp wrote:
             | Lies, 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 wrote:
               | 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 :)
        
           | tadfisher wrote:
           | 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.
        
         | dingi wrote:
         | 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.
        
           | tadfisher wrote:
           | 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.
        
         | rileymichael wrote:
         | 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
         | 
         | https://www.youtube.com/watch?v=IUrA3mDSWZQ&t=2626s
        
       | cekrem wrote:
       | 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).
        
         | pjmlp wrote:
         | That is always the gotcha with guest languages.
         | 
         | C++ cannot get away from C, Typescript cannot get away from
         | JavaScript, and so forth.
        
           | zaphirplane wrote:
           | That 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 wrote:
             | C++ 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.
        
       | armchairhacker wrote:
       | 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.co
       | m/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 wrote:
         | 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.
        
           | HendrikHensen wrote:
           | > 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.
        
           | tester756 wrote:
           | >Exceptions are cheap on the happy path and super expensive
           | on the error path.
           | 
           | Depends on the software, huh.
        
         | loglog wrote:
         | 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.
        
           | ajrouvoet wrote:
           | This last point is the key observation.
        
         | Tyr42 wrote:
         | I think bridging this syntax onto legacy checked exceptions
         | from java would make a lot of sense.
        
       | esafak wrote:
       | Slated for release in 2.4 this summer. Track in
       | https://youtrack.jetbrains.com/issue/KT-68296
        
       | ixtli wrote:
       | 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.
        
         | loglog wrote:
         | 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.
        
       | jpalepu33 wrote:
       | 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.
        
         | HendrikHensen wrote:
         | 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.
        
           | Tyr42 wrote:
           | 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.
        
             | HendrikHensen wrote:
             | 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.
        
           | josephg wrote:
           | > 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.
        
             | HendrikHensen wrote:
             | 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).
        
         | hackthemack wrote:
         | 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.
        
       | spankalee wrote:
       | 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?
        
         | hutao wrote:
         | 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)?
        
           | spankalee wrote:
           | 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.
        
         | thesz wrote:
         | > But I'm unsure if there's a strong argument that resonates
         | with me yet that the language shouldn't have exceptions at all.
         | 
         | Result <E, T> is the type _Either e t_ in Haskell. And Either
         | is a Monad: https://hackage-
         | content.haskell.org/package/base-4.22.0.0/do...
         | 
         | 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_ ).
        
         | mamcx wrote:
         | Alternatively look at https://dlang.org/articles/exception-
         | safe.html.
         | 
         | 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...
        
       ___________________________________________________________________
       (page generated 2026-01-23 23:01 UTC)