With respect to Result, I know a concern is that i...
# language-proposals
n
With respect to Result, I know a concern is that it erases the type of the error. Has there been consideration to making Result an interface, and having a derived class:
Copy code
interface Result<out T> : Serializable
inline class Outcome<out T, out E: Throwable> : Result<T>
If you do something like this, then you can use
Outcome
as local error handling wherever you want to keep the type. And, if you need to collect errors from multiple sources, you just let Outcome up-convert to a Result. Things like
runCatching
could construct an
Outcome<T, Throwable>
(i.e. the most general case of Outcome) which could then be allowed to up-convert to Result instantly (leaving their API identical to now)
m
I think this is for #stdlib. Using an
interface
would cause a lot of unnecessary boxing of the
inline class
instance.
g
It would not allow to make Result inline class, so I don't thinks its really would be an option Adding exception type was mentioned in KEEP, considered for Result if some day Kotlin will support optional generic types
j
I think @elizarov said something about it in Reddit AMA. IMO result should be something like Either from Arrow instead of using for the error type only exceptions. This post should be enough to move on in that direction https://medium.com/@elizarov/kotlin-and-exceptions-8062f589d07
n
@Marc Knaup i think the cases where that performance hit is relevant, people can opt to use something else? the more important thing is to offer a solution that works well for people software wise, IMHO
m
@Nir
Result
is used for every single coroutine continuation. So it’s used pretty extensively and in a very performance-critical situation.
Result
isn’t made available as a general purpose class. Your suggestion should likely be its own type, at least until Kotlin has more capabilities to make a single class a good fit for all use cases.
☝️ 1
n
Ah good points
Yeah, I I hadn't worked much with coroutines
I was thinking about purely an error handling situation. A separate class would make sense.
@Javier Yeah, the thing is that people in Kotlin keep saying "don't use exceptions, use some kind of sum type for most error handling", but then the standard library doesn't provide one
m
Functions are not allowed to have
Result
as a return type at the moment. The uses are mostly limited to coroutines and
runCatching
at the moment afaik. I agree that error handling in Kotlin is not that nice. I like Swift’s approach.
n
e.g. string to int can fail in at least 2 distinct ways, so a
toIntOrNull
method isn't that great.
m
Yeah all those
OrNull
are quite annoying. Seems like a workaround.
n
@Marc Knaup i think it would be reasoanbly nice with some kind of approach like what I outlined. the API for for Result isn't bad.
m
The API is highly optimized for the current few use cases and I guess not guaranteed to be stable.
n
I mean strictly speaking, what makes the most sense IMHO is to have
toInt
return
Outcome<Int, ParseError>
(or whatever), and then you either call
getOrThrow
, or
getOrNull
, etc
So with inline functions you can get "inline early return as an expression", which seems like what a lot of these sum based systems are after
e
On JVM this approach does not make a lot of sense, since it will make every
toInt
operation very expensive. Hence we have a compromise with a
toInt
and
toIntOrNull
combo that this both efficient and pragmatic.
☝️ 1
m
That Either approach also comes to its limits with business logic. What’s an error and what isn’t? What about multiple errors? E.g.
Int | NotApplicable | List<Error>
etc.
n
The main thing that is missing right now, IMHO, in terms of what can be expressed even with core Kotlin, is a super ergonomics way to take a Result type, and give you the value if it exists as an expression, or else return early with failure
m
@elizarov if there would be a language construct for that then the compiler could optimize that, right? If unwrapped directly. It just wouldn’t be Java-compatible anymore or needs a workaround.
n
basically, what you would currently do now with
result.getOrElse { return it }
this is a bit verbose IMHO
e
Note that is addition to performance problems if
toInt
would be defined to return some kind of
Outcome<Int, ParseError>
then it would be very cumbersome to use in a typical situation when you know that input must be an int and you just want your program to crash on invalid input. This is like happens in 99% of code where parse anything, etc. There is really no much sense to be forced to write the code to say "I just want to crash here" every time you attempt to convert a string to an int.
n
@elizarov i admit it's not immediately obvious to me why that's so much more expensive, but I'm willing to take your word for it 🙂
Well, that comes back to my other point
languages that encourage sum-type based error handling tend to have ways to make this super easy
m
I Swift you’d use
try! something()
. The
!
makes it explicit that you expect it to not fail. Or
try?
to return
null
on failure. It’s just a little annoying to use because you have to put it in braces for longer chains.
e
They still force you to write boilerplate, even if this boilerplate is small. What's the pragmatics of that if that is what you wanted to most of the time anyway?
In Kotlin you just write
something()
for a typicall(sic!) use-case of
try! something()
and write
somethingOrNull()
for a rare use-case when you need
try? something()
n
Well, it makes it explicit that errors could occur there. But sure, your point is taken that the shorter name (toInt) should be the most useful.
That said, it still makes it somewhat awkward in Kotlin to deal with the case where you do want to examine the parse error. You either try/catch, which isn't as nice (IMHO) as using
when
and pattern matching
m
@elizarov yes, but you’d have to duplicate all functions. And you lose error information in the process. Especially since we’re told to avoid exceptions 🤔
e
When you parse stuff, in particular, the error can occur everywhere. There's no much use in making it explicit on each like, just like it does not make sense of writing
await
on each line when all your code is doing asynchronous stuff.
n
or you do
somethingOrNull
. IMHO
somethingOrNull
just isn't that useful.
What's the advantage of
somethingOrNull
vs
trySomething
that returns something similar to a Result<Something> ?
e
The Kotlin solution is not without its own problems, for sure. For one, it is cumbersome to define those
something()
and
somethingOrNull
pairs yourself. Not a big deal for apps (you usually know which one you need), but could be a bit of a pain for libraries.
m
await
has its own language feature: suspending functions. There’s no such thing for error handling.
n
Yes. I'm not very familiar with the performance trade-offs in Kotlin/Java, in C++ though I've advocated for a while to have something like `Result`; the idea is that you just define a function returning Result and taht's it, people who want it to throw can do that with an extra function call.
The ideal to me is to define a single function that is ergonomic both for immediate error handling (via something like
when
), with actually error informaiton (not optional), and also ergonomic to convert into an exception for non-immediate error handling
m
I don’t see any consistent way of approaching error handling in Kotlin right now. On one hand we move away from exceptions, on the other hand most of kotlinx libraries rely extensively on it. And stdlib copes with OrNull variants.
n
One random though; would be to "bless" the error type with some of the same syntactic sugar as nullables
e.g. have
!!
give you the result or throw
e
stdlib does not really need rich errors. success/failure is enough.
n
err really?
That's a bit surprising tbh. If you fail to open a file, you want to know what the path was that you failed to open at. If you fail to parse a string, you may want to know whether the string was incoherent, or simply to large to fit in an integer.
e
For domains where you have some more fine-grained errors, you can always define your own domain-specific error type and domain-specific result type. Kotlin DSL capabilities (HOFs and stuff) provide lots of opportunities to define a nice-looking and conscise API.
n
lots of examples of things that are in the standard library that have multiple failure mods, IMHO.
e
Kotlin stdlib does not really have files support. Files are very domain-specific (most of the modern apps don't work with files)
m
I agree that stdlib in most cases doesn’t need to be more fine-grained.
n
Sure, I agree. I just think it comes back to what Marc said, there isn't enough of a consistent approach that's offered. The standard library really only offers exceptions, and nullables, yet, blog posts from core devs are suggesting that neither should really be used for errors 🙂
m
But if every library and project has their own error approaches, composing them becomes a re-wrapping nightmare, as there is no standardize baseline.
n
What eventually can happen is that every third party library reads that blog post (was it yours?), does something similar, but not quite the same
Right, exactly
C++ is very similar btw; the core language has very powerful capabilities to define error handling, lots of people do so... but nothing is standardized except exceptions really
e
In your domain you can have:
Copy code
foo() -- return domain-specific result
foo().orThrow() -- to throw exception on error
foo().orElse { return it } -- to return error (like ? in Rust)
when(foo()) { ... } -- to decomose it
// etc
n
and it's a huge headache for third party libraries. IMHO, error handling is really one of the most valuable things to standardize. From a "vocabulary types" perspective.
e
But different domains DO have different needs.
m
Right now I don’t even know what exceptions Ktor throws, just that it throws some. Same for kotlinx-serialization. If they add their own result types - fine. But then I also have lots of re-wrapping instead of just saying “bubble up whatever error you have”.
e
If you work A LOT with files it is one thing, if you casually work with files that's another.
n
in some cases, sure. But if you were to define some kind of error type/interface in the standard library, then many domains would be ok to use that. The ones for which it is not good enough, would at least be encouraged to provide their own functions mapping back to the standard error types
e
If ktor throws an exception it usually means it had crashed beyond recoverty (aka "panic" in Swift). A result would not have helped here at all.
m
ktor throws for every single connection failure 🤔
or parsing issues
normal network stuff
n
If you had something like what I suggested,
interface OutcomeBase<R>
and
inline class Outcome<R, E> : OutcomeBase
, then some classes would use Outcome directly (making life easier), some would not but might use the OutcomeBase interface, some would use neither but provide getters that returned outcomes... etc
m
(I’m on the client side here 🙂)
n
The idea isn't that 100% of third party libraries would use all of it as-is, but 80% would use most of it, and the remaining 20% provide mappings... translates to a much easier user experience IMHO
m
Anyway, it’s not an easy topic. As you’ve said, domains are very different and finding a common denominator to establish a basis is difficult. I just feel checked exceptions, unchecked exceptions and Swift’s
try
are all closer than anything Kotlin has to offer.
e
The challenge here is that it makes little sense to add such things to stdlib when stdlib itself don't have much use for them. If ktor problematic? Then lets work with ktor team to implement a consistent and nice solution inside ktor and then maybe other libraries will adopt it if they see it is good.
n
Yeah. I agree with
try
, not necessarily that I think it's the perfect approach, but the basic assertion that error handling is something work expending language design real estate on
I see. I think there is value in setting a standard for users, and other packages, even if it's not used nearly as much. It's almost like... a relief, you start using Rust or Swift, you want to handle errors, you look it up, and you realize there' sjust a simple standard way of doing it, you do it without worrying too much, at least initially. can always revisit if you have special needs (which you usually don't).
m
Shouldn’t either Kotlin as a language or the stdlib help facilitate a “common” approach for something that’s as fundamental as raising and handling errors? What programs are there that won’t need that? 🤔
3
e
error handling used to be and still is a corner-stone of the design of low-level language that have to deal a lot with files, owning to the UNIX era where every app would read and write some files. We're in different era now. Most of Kotlin apps don't care about file. They care about network. True. But network is a whole different beast.
n
But you can still get all kinds of different errors from the network
(not sure if networking is in the standard library or not)
e
Networking is not a part of the Kotlin stdlib either.
n
Right, fair enough. I dunno, I think ultimately even if the standard library does not need it much, because of simply being a small standard library, there's still huge value in standardizing a common approach. The approach can be quite open-ended and flexible in many ways, and my guess is that most third party libraries would use it.
That definitely seems to be what happens in other languages. E.g. Rust, seems like pretty much all third party libraries are using
Result
, or in some cases building on top of Result
I can tell you some horror stories about C++ sometime, maybe that will help 😉
e
Shouldn’t either Kotlin as a language or the stdlib help facilitate a “common” approach for something that’s as fundamental as raising and handling errors?
What programs are there that won’t need that?
But Kotlin is facilitating a common approach: • If that's a programmer error, throw an excaption. It just like "panic". You program shall crash/log/report it/wahtever global action you have for cases like that • If that's an input or external error then simply return as a result of your function and process it at this function's call site. The whole Kotln stdlib is centered around this simple approach.
n
Well, for the second approach though, there is no help with respect to defining the type, even though there's clearly a lot of room for widely reusable code there
m
I remember that I’ve tried a custom Result type for my graphql library. The constant wrapping/unwrapping and collecting of errors and mapping of values made it so difficult to reason about my code that I’ve removed it again. At some point I even had results wrapped in results without noticing. Even if we’d have a general Result type @Nir using it as a replacement for exceptions is plenty of work.
n
Not suggesting it as a full replacement though. They each have their place. My point is mostly just, probably most third party libraries can make use of a common
Result
type. It doesn't really make sense to provide nothing, and tell people to define their own type for your second bulleted approach
m
@elizarov it’s anything but obvious to people, otherwise most libraries, most notably everything kotlinx, would use that approach. But none is doing so.
n
when 95% of people are going to start with
sealed class Whatever { class Success, class Error } ....
m
There must be a reason nobody is using that approach.
☝️ 1
e
Well, for the second approach though, there is no help with respect to defining the type, even though there's clearly a lot of room for widely reusable code there
But for majority of cases you don't need any additional details on the error and the nullable type works well. The rest, when you need additional error details, are very domain specific. What's the problem with a domain-specific sealed class for that?
n
Well honestly not being pushed by stdlib is a big one. It makes a big difference having
Result
in your standard library vs advice to write it from scratch, IMHO
The problem I think is that, each project will define their own, and have similar but not identical APIs, and they will also be of different types
this will create headaches for users, having to work with 3, 4, 5, etc different
Result
APIs that are slightlyd ifferent, and a bigger headache when errors need to be propagated up
e
What's the problem with that if their error details are different and not compatible anyway?
n
if lib1 and lib2 use slightly different approaches to error handling, andmy function foo calls things from each... well now I have to either throw, or write my own class that can wrap both lib1 and lib2 errors
I mean I think the point is that there is common API even when error details, are diferent?
Outcome<Result, Error>
is a generic class
the
Error
can be vastly different from library to library, it can have literally any API they want
e
If lib1 and lib2 belong to the same domain I hope the'll agree on some domain-specific approach. In kotlinx world, for example, we are writing only general-purpose libs, so we did not even had a need for any kind of result type so far, since we don't have any error details that would have made sense to return.
n
but you still get a common API in terms of the different ways of getting the value or throwing, getting the value nullably, getting the value or defaulting, etc
I mean IME even if lib1 and lib2 have very similar needs it's very common if they're written by different people at different times, they will not have identical approaches
e
If the only "common" thing you have is throwing exception or returning, then it basically means your errors are just panics. Just throw excepitons on error then and don't complicate your code with result types.
n
but I have trouble understanding why something like
Outcome<result, Error>
which is generic wouldn't work across many domains...
That's not the only thing. I mean, almost the entire current API of
Result
are things that are fairly useful
I mean I think it helps to look at real world examples. I think the idea of common error types in Rust and Swift have been a big success, and the lack thereof in C++ has been a big problem.
m
We kinda had a good mix of generic error plus domain-specific errors with exceptions. Success: value Failure: exception Domain-specific: type of exception I know the exception approach has its problems but at least there was a common baseline. I could always either handle the specific exceptions or widen my own exception type for a higher execution level. I had great tools for handling domain errors, general purpose errors with mostly nice syntax. Would I have do that without exceptions I’d have to flood my library and API with custom types and handling them becomes quite cumbersome. There’s just no nice way to work with Result types, be it a standardized one or a custom one.
Copy code
a(b(c()))
would become something like
Copy code
c().mapValue { b(it) }.mapValue { a(it) }
throughout the entire codebase
And that would just be a generic Result type. It would be way more complex with mixed Result types.
☝️ 1
e
I mean I think it helps to look at real world examples.
💯
m
There aren’t many examples because the approach is rarely used 🤔 It’s still a mix of
null
,
catch
and a few
Result
. I would’ve used my GraphQL library as an example but I’ve already refactored the result approach out as much as possible. Right now the situation is mostly random exceptions throughout the app that get fixed on the fly as they occur. Everyone’s migrating from one world (checked exceptions) to another (all unchecked) to another (use domain specific types, just not documented afaik).
Also, speaking of the performance of Result or
OrNull
. Using a sealed class causes wrapping allocations, which makes it expensive in high performance situations.
e
If you care about good-case (non-error) performance on JVM, then you'll have to throw exceptions on errors.
n
Yeah, I think in real life what happens as a result of stdlib not providing
Result
, is that people feel it's not encouraged and they just use exceptions
m
Damn JVM 🙄
n
good for ergonomic happy path, but it means if you want to handle errors locally, you have to read docs, not ge thelp from the type system, and use relatively awkward syntax
m
I short-cut exceptions with
runCatching
sometimes 😛
e
It all depends on real-world use-cases. The devil is in details. When you say "they just use exceptions" the key question here is "they use exceptions for what?". Maybe they just use exceptions for what exceptions are designed and well fit for -- to indicate that a program has encountered a (non recoverable) error, should report it to the support team and restart/terminate.
m
That’s Kotlin’s (rather new I think) definition. So far exceptions have meant any kind of error. Esp. when you consider Java interop. The first (and only) time I’ve heard that exceptions should’t be used for expected errors was in one of your Medium posts. Unfortunately there was little to no guidance what to do instead. Either there should be great error raising/handling support by the language or if it’s already possible there should be good documentation. If it’s the latter then maybe I just haven’t found the right approach yet.
1
And the fact that all kotlinx libraries don’t follow the exception = fatal error approach just causes more confusion.
n
That is a pretty surprising definition of exception
especially surprising in a language that doesn't define any other standard way of handling errors
m
I keep looking at what Kotlin’s own libraries are doing and adapt to that to write idiomatic good Kotlin code. I think a lot people do. But in this case, it doesn’t work.
Whatever the solution or options in error handling are, they should be consistent and well-documented. I think starting from that helps deciding what to do next.
What options are there and when to use what approach. And how to translate between them.
OrNull
vs
throw
vs
Result
vs
sealed class
And stdlib & kotlinx libraries should be perfect examples of how to do it “right”.
❤️ 1
r
I think union types can handle most of these use cases but require deeper lang integration in the type system hierarchy. For java compat perhaps union types can desugar into a special tagged union like the one we have in meta. The use case of returning N types when the types are not in the same hierarchy used extensively in networking, http etc in scala is covered by both tagged and untagged unions. I think this would be perhaps good to explore for Kotlin since they can also be optimized unliked sealed classes. Construction of a value of a union and extraction has potentially no cost if implemented in the compiler and IMO are much easier syntax wise than tagged unions.
👍 1
Some examples of what those look like https://github.com/arrow-kt/arrow-meta/blob/master/compiler-plugin/src/test/kotlin/arrow/meta/plugins/union/UnionTest.kt I dislike that are nested like
Union<A, B>
and would be much better to have as infix types with
|
A | B
f
I agree with the sentiment here that error handling in Kotlin is not well defined, especially on JVM where all Java APIs throw exceptions for everything (which is what I do too because it feels right, instead of working against Java and ending up with an inconsistent API). The error handling story in Rust isn't as great as advertised here and also the reason for libs like anyhow and thiserror. But the Rust knows and acknowledges this fact and created an error handling working group. Some issues in Rust are exactly those mentioned in this thread: - You need to write your own Unions all the time - You need to wrap other errors correctly - You need to map around all the time - Option and Result are not compatible (
?
) - ... A Result type comes with its own issues, and it's actually identical to checked exceptions. So is the union type idea. The only difference is that any
throw
directly starts unwinding the stack, a return doesn't. Yes, there's a difference in syntax (try/catch) but this is artifical and doesn't need to be there. Also, who said that exceptions are limited to inheritance. One could also define them as an enum or sealed class, or whatever. I think it's possible to find a solution that makes everyone happy (ergonomic) and allows Java interop as well as great performance but it would need to be exception based to achieve that.
👍 1
n
@Fleshgrinder I mean
anyhow
is more or less a more sophisticated version of what I suggest here with
Outcome<T, E>
inheriting from
OutcomeBase<T>
or whatever
basically, there needs to be an easy way to do type erasure on the error type parameter, for bubbling up errors.
Rust doesn't give you any out of the box solution for this at all, which is why I think
anyhow
exists.
I'm not sure if
inline class
mitigates type erasure like it can for functions. If it could, that would be helpful as it would allow downcasting out of the box (as
anyhow
does)
f
But you'll have a lot of new issues with this approach (those outlined by others above already). My point is that we might need a totally different approach that brings all requirements together. This won't be the hardcore functional way, but also not exceptions as we have them in Java. The biggest issue I have in my mind for such a solution is in regards to avoiding the stack unwinding but still be JVM compatible. However, if we would say that we can live with this then it would boil down to simply changing the Kotlin syntax and compiler. We'd introduce
throws
as part of signatures but only allow one throwable in there. Now we have the equivalent of what's proposed here in regards to making errors explicit. In code
fun f(...): Result<T, E> {}
becomes
fun f(...): T throws E {}
Now on the calling side we can have
f(...)!
to simply let the error go up,
f(...)!?
to ignore and turning it into
null
, or
f()
in which case you either get
T
or
E
(back to union) and need to check on your own. All of this is transformed to standard try/catch in the byte code. So from a Java perspective nothing actually changed. This is a very rough idea from the top of my head, so be gentle. 😉
n
yeah seems a bit similar to
try
in swift or the proposed static exceptions in C++
f
Not very familiar with those, from what I've read here Swift is not easily chained. With the proposed syntax this should be possible though:
x.a()!.b()!?.c()?.d()
m
!
and
?
are for nullability, not for errors. That’s would be ambiguous and confusing. What would happen if you mix a nullable type with an error-returning type? Anyway, the status quo of error handling is quite chaotic. I suggest to properly organize that first an only then improve upon it. Otherwise we have an unstable foundation for further improvements.
f
Currently we have
!!
and
?
but not
!
nor
!?
and the symbols as proposed would be very close in semantics: -
!
throw if error -
!!
throw if null -
?
nullsafe call -
!?
null if error and nullsafe call I didn't come up with this as an improvement but rather as an idea to properly organize things, but while taking all requirements (except my own "stack unwinding" issue for which I don't know any solution without additional object creation or giving up Java interop) into account.
One could argue that
?!
is more logical than
!!
for throw if null. I didn't propose that because it would require a depreciation of a well established operator and it could probably only be removed in Kotlin 2 but I'm sure not many would like this for such a simple thing. The biggest problem really is the unwinding, using exceptions like this means that you use them for control flow too and this is wasteful. In Rust a lot of thought and effort goes into making their Result as lightweight as possible because of this.
m
That’s a lot of syntax magic. Difficult to read already 😅
f
Actually I'm wrong, you're right. We'd need
?!
as well for nullsafe and throw on error. `T`otally, looks like Perl. 😆 But there's no other way to overcome the cumbersome issue mentioned by you and @elizarov. Either you have short easy syntax or, well, nothing, like we have it now.
n
not really, an alternative is to basically have a "block scope" syntax
basically something like
val result = withError { a(b(c())) }
basically this is a bit like what
runCatching
+ exceptions give you
f
In that case you can just stick with try/catch too.
1
n
not really because this only works nicely if a, b, and c are throwing exceptions to start with
and most people think it's preferable if individual, lower level functions return something like an
outcome<T, E>
, that way if you decide to handle errors specifically for a function, you get the benefits of type safety
f
You'd have type safety in my proposal. Either
T
or
E
and you need to check first. You just don't have an explicit wrapping type to avoid the boxing and keep normal Java signatures.
n
it's not really type safety
it's the difference between
outcome<T>
and
outcome<T, E>
It's advantageous to have functions that can error start with the most strongly typed thing, because throwing away type information is easy and cheap, recovering it is hard. However, it needs to be sufficiently ergonomic to throw it away.
f
The block syntax is as cumbersome as try/catch is what I meant. 😉 Why would
T | E
not be type safe if you have the required compiler machinery? Have a look at Ceylon, they really mastered this.
n
Sure, T | E is type safe, if you can control E
if you are simply annotating functions with
throws
, then you aren't specifying E individually though
you are basically just assuming some error interface/protocol and that's it
Ah, you want to allow specifying the E, my bad
f
I'm not following...
Result<T, E>
and
T | E
are equivalent...
n
fun f(...): T throws E {}
f
👍
n
yeah, it's just
throws
is already used often in languages, without the
E
which is why I was confused
f
Na, that be useless.
n
I don't agree that it's useless, just serves different purposes. Swift has that. For me the basic idea idea is that error handling usually comes in two flavors. There are clients who want to handle the error up front, and they want the biggest combination of type safety and ergonomics. And there are people who just want the result, and let the error auto propagate to some parent. And the question is how to help both those use cases as best possible.
f
The solution for those who want to get rid of E is already there. Just throw it.
n
i guess, throws is useless in Kotlin specifically since any function can throw already, at any time 🙂 In swift, if you don't mark something
throws
, you're forced to handle the errors
Right
I think swift's approach is actually pretty elegant because it's actually zero boilerplate at the function level.
If a function is marked
throws
, it auto unwraps everything. It's basically like my scope idea, but for the entire function scope
If it's not marked throws, you have to handle things explicitly.
f
But you loose the type and the explicit. But that's what people want to have.
Well, me too actually...
n
well, that's the equivalent of exceptions in kotlin though, which also lose the (static) type, and are implicit. It's the second use case, like I said, where people want errors to auto propagate
I guess a lot of this comes back to: do you default to implicit bubbling up, and have to opt into detailed, immediate handling of errors
or, vice versa.
In swift, you have to opt into implicit bubbling up, but the boilerplate to do so is minimal. In kotlin, i guess we are in a model where things bubble up implicitly. So maybe what we need, for your
throws
suggestion, is special syntax not to throw the exception, but rather, special syntax to ask to handle the E explicitly.
If that makes sense.
Or, better yet perhaps, just make it happen automatically when you use it with
when
so if a, b, c are all marked
T throws E
, as you suggest. Then
a(b(c()))
"just works" and if there's an error it throws, just like normal exceptions throw now
but if you have
when (val x = c()) ...
then there's no exception, and we automatically start pattern matching on
T
and
E
just a thought, I guess
I should ask someone who knows the JVM better if there's a fundamental problem with this
f
And what anyhow provides in Rust, this functionality definitely needs to stay.
i
but if you have
when (val x = c()) ...
then there's no exception, and we automatically start pattern matching on T and E
Is it possible to distinguish this way when
E
is thrown from when it's returned?
n
well, under this proposal,
E
would be thrown
you would annotate
fun c(): T throws E
or something similar
in the body of
C
you would either have to return a
T
, or throw an
E
I thought about it more though and probably an explicit syntax is best; you can't always count on immediately pattern matching
but this syntax is only for people that plan to explicitly handle the error locally. People that want to let it propagate can just do business as usual.
You could imagine the syntax being say
!?
or something like that
i
either have to return a T , or throw an E
But what if
T
is the same as
E
or its supertype, e.g.
Any
? For example, if you have a
List<T>
and store exceptions in it, would it be possible to distinguish an exception thrown by hypothetical
first(): T throws E
from an exception returned by that
first()
?
n
Well, the pattern matching could work as matching
T
or
Failure<E>
, or something like that, where
Failure
is some type not accessible to users i.e. it's just there to ensure that the error type is always distinct from the result
m
I actually had the issue that @ilya.gorbunov mentioned, just slightly differently. When dealing with a
Result
type in a scenario where you want to return
Any
or
Any?
then it's all to easy to accidentally create a
Result<Result<Any>>
. Here also the distinction between what is a the actual return value and what is a success/error wrapper becomes difficult and mistakes happen very easily.
n
I have been digesting all this for a bit. I now wonder if the solution is "opt in" checked exceptions, essentially
If we think about the problems with checked exceptions, it's basically a lot of verbosity, lots of types hitting functions that don't know how to deal with them. This isn't an issue if it's opt in. Maybe I'll try to explain my idea in a new message in the channel
okay, but roughly, lets say we have something like this:
Copy code
fun String.toInt() : Int throws FormatError, CapacityError { ... }
Despite appearances, normal usage stays the same:
"hello".toInt()
compiles and throws an exception. However, if users want to they can opt in to static checking. They do this with some sigil. Let's say
!?
for now. when
!?
is used. you get the behavior of checked exceptions. Meaning: if it's outside a try block, it propagates the exception, and the current function needs to have compatible
throws
. If inside a try block, it only tries to propagate any exceptions not handled.
Copy code
fun foo() : Int { return "hello".toInt()!? } // Doesn't compile
fun foo() : Int throws FormatError, CapacityError { return "hello".toInt()!? } // compiles, propagates errors
fun foo() : Int { return try { "hello".toInt()!? } catch (FormatError) { 0 } catch (CapacityError) { 100 } } // compiles
fun foo() : Int throws CapacityError { return try { "hello".toInt()!? } catch (FormatError) { 0 } } // compiles
Overall the main use case is when you are calling functions where you genuinely plan to handle most/all failures modes.
Could adapt this to use
when
instead of try-catch, or even something else, but the basic idea remains the same. library APIs can optionally annotate statically the errors. If they do, then users can opt-in to static handling.
f
Directly using
when
would not work because your function throws, you have to convert it. The idea is that the byte code in the background still is normal try/catch. Without special handling the solution already exists in std (kind of):
Copy code
when (val x = runCatching { f() }) {
    Success => a(x)
    Failure => b(x)
}
I personally don't like this syntax because it feels unnatural, having special handling in form of a normal function could work here too (the opposite of Rust's
unwrap
):
Copy code
when (val x = f().wrap()) {
    Success => a(x)
    Failure => b(x)
}
Here
wrap
would need to be understood by the compiler so that it can turn it into try/catch.
Copy code
try {
    a(f())
} catch (E e) {
    b(e)
}
Or we directly special case any such function if it is used in
when
and simply assume that the user wants to match on
T | E
and not on
T
. Although, I think that an explicit
wrap
is more useful because you can turn the result of any such function into a
Result<T, E>
explicitly everywhere, and it does not only work in
when
. So, yes, we can distinguish them.
Quite old but still of interest for the
toInt()
and
toIntOrNull()
performance discussion (which could become
toInt()!
and
toInt()!?
as the idea/proposal currently stands): https://shipilev.net/blog/2014/exceptional-performance/ The biggest advantage I see here is that we would not need to write two functions for everything anymore that work slightly different just to make the unhappy path faster. Even something simple (on the surface) as parsing a string to an integer can fail at multiple places and it would be nice to tell the user what went wrong. We have two options here, write the
toIntOrNull()
first that always simply returns
null
and wrap this in
toInt() = toIntOrNull() ?: throw
where all we know is that parsing failed, that's it. Or we go the other way and implement
toInt()
and define
toIntOrNull() = try { toInt() } catch (E) { null }
which would probably be optimized away anyways and give us the same result but we could tell the user exactly what went wrong where. Or we write two functions, which is what we currently have in std, that might suffer from incompatibilities and diverging changes. I really don't think that optimizing for the unhappy path is so important, especially considering the above benchmarks for exceptions.