[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)