What is the current opinion on error/exception thr...
# announcements
j
What is the current opinion on error/exception throwing in Kotlin? For me I try to avoid it as best as I can because there are no checked exceptions and therefore no IDE or compile time safety whatsoever. I wrap any source of exception (like a library call that might throw an error) into a Result object to avoid having unexpected errors thrown in my face. This is especially interesting for libraries. How do I know that a certain call might throw an error? It is therefore curious to me why there is so much support for catching errors, while it is impossible to know which piece of code might actually throw an error, without prior knowledge of the code itself. For example, Flows support error catching, but there is no way to know which operation might throw an error. So what is the "best" option? Should I wrap any error in a result object directly from the source (by wrapping library calls in try/catch)? Or should I throw unchecked errors and hope for the best that someone/some code down the line will catch them? Is there any pattern in this that is advised?
n
FWIW I find the messages pretty mixed. A lot of people suggest doing sum type handling in Kotlin, including Kotlin devs, but there is no result-like type in the standard library for this purpose
I think the basic idea is that you define sealed classes to carry result/error for your domain as it makes sense, but I think this ignores the fact that really there's a lot of common functionality you want on these types, like what you find on Kotlin's Result type (which isn't really usable)
The blog post above I find a pretty good example of that; if the recommendation is really to use sealed classes, why isn't the standard library helping with a generic class to standardize it? The string example is a good one too; string -> int can fail for two different reasons so toIntOrNull() is not a solution
m
imo it’s chaotic at the moment. Even different official libraries handle this completely differently. In own code I’ve used a lot of exceptions for communicating user errors (not program bugs). I just don’t see an alternative in adding an insane amount of sealed classes and wrapping. When it comes to external code it depends on the context and the library if I’m super curious and catch all
Exception
or if I don’t catch anything. But there’s no unified solution yet.
n
Different libraries will keep using their own solution until the standard library provides something standard that's good enough to make 90% of people happy (not that hard probably), and then that will be the unified solution, with only libraries with very particular needs deviating from it
My idea is something like an
interface Outcome<T>
that erases the error type to simply something throwable, and then
class Result<T, E> : Outcome<T>
or something like that, where E can just be a simple class that Result can wrap into a throwable, or something like that (might be tricky because of type erasure)
j
Yeah I keep on using my sealed Result<T> class (Loading/Success/Error) until something betters pops up. it does not throw the errors, but propagates them so you can handle them regardless. The only place I see error throwing as useful is a pipeline in a backend, as any logic/input failure will result in a pipeline failure anyway, but for frontend apps you cannot just make the app crash like that.
m
@Nir I'd still have a crazy amount of wrapping and unwrapping
â˜đŸ» 1
j
I think
Either
could be added to the stdlib
n
I'd prefer something like rust's result which is explicitly asymmetric
Either IIUC is only <result, Error> by convention
but basically the same idea
@Marc Knaup why would you have a crazy amount of wrapping/unwrapping? Just trying to understand
I think in some of those cases, if you don't want to deal with wrapping/unwrapping, it's still completely fair to throw... after all, exceptions are the fastest way to bubble up ultimately if you don't want to deal with handling the error in the next couple of layers
m
If you have a hundreds of functions each calling several other functions, every single call result would have to be unwrapped, errors returned and values be used for the remainder of the function.
I had that in one of my libraries. It became so bad to maintain and understand that I’ve removed it almost completely and went back to exceptions.
Even worse is when you’re dealing with
Any
results because you end up accidentally creating nested
Outcome<Outcome<
>>
I had to add runtime checks to find these bugs
j
Arrows Either is just something like Either<T, R> I think. You could use whatever you want for left and right
j
Well railway oriented programming is an interesting concept as well -> https://proandroiddev.com/railway-oriented-programming-in-kotlin-f1bceed399e5
m
@Javier yes, that “use” is likely “unwrap & wrap” - potentially hidden behind a “mapResult” function or alike.
j
should avoid nesting too much crap
n
@Marc Knaup I think i fyou aren't planning to handle an error "soon", then yeah throwing is better, at least without massive syntactic sugar
m
I pretty much prefer the approach that Swift is going with errors. It’s explicit, untyped and very easy to chain.
j
Are you talking about a
fold
function?
m
@Nir yeah but when I mix both approaches I make it worse than just following one or the other - imo. Otherwise you may easily confuse it in a function and forget a try
catch for example.
n
the best you can do in Kotlin is something like:
Copy code
fun foo(): Outcome<Int> {
    val x = bar1().getOr { return it} + bar2().getOr { return it }
}
without using exceptions at all
m
yeah I had that
super annoying
We need something like that, but as a well-integrated easy language feature
n
Right. Like
?
in Rust.
m
Which is basically Swift’s
do
catch
n
But ultimately it really depends on whether you want it to be typed or not
and to what degree
I should read more on swift's approach
I think actually there's a big opportunity to have more strongly typed error handling, but also provide the automated tooling to make it easy to do the refactoring automatically... which is a place where you can imagine a language like Kotlin stepping up
the tradiitional justification for exceptions is that most functions are "error neutral", and if your exceptions aren't untyped, then every time a low level function changes its error signature, you have to bubble up a lot of changes by hand. But if it were done automatically, that would really change the equation.
m
I’d say if you need types - use
sealed class
. if you don’t - Kotlin needs a solid concept for that.
n
If you don't want types, what's actually wrong with exceptions though?
being implicit?
m
They’re not checked
n
checked = typed though
m
Anything can throw anytime and you don’t know when to expect it
Checked doesn’t imply typed
Just that you have to catch something
(or rethrow)
n
maybe you are using typed in a more specialized sense, but in the broadest sense, that's exactly what it implies
typed means its part of the type system, the type system are all the things that have to be correct before the program can even be run
checked exceptions in Java are part of the type system, effectively
m
Type is “is this a parsing error, a schema error or an input validation error” Checked is “this error must be handled” Unchecked is “you don’t have to handle this error” (most likely programming errors)
n
That's just an artificial narrowing of what a type system is...
type systems include many things that fall outside of that definition
the type system is just anything that's checked statically. Can include lifetimes in Rust, side effects in Haskell, error handling, etc
you can see in Swift, whether or not a function throws is literally part of its type
m
Then you could also argue that exceptions are always typed, otherwise they wouldn’t be exceptions đŸ€” Every language feature is basically a type. That’s way too broad
n
no, it's not broad at all... unchecked exceptions are not checked statically
ergo, not part of the type system
very straightforward
m
You can’t throw
Int
in Java. So exceptions must be typed somehow
n
there are restrictions on what can be thrown, if you want to think of it that way the "throw" keyword itself has some type safety.
m
Anyway, the terms used aren’t that important. Important are the major categories of use cases.
n
I'd suggest, if you like Swift's approach, you click on that link, because it will show you the full implication of making these things checked
It is useful but can also be painful sometimes, any time you have a higher order function now, you have to consider whether or not the function you're accepting can throw or not
currently in Kotlin you don't have to worry about this; you just pass the function and if it throws it bubbles up to you
m
a) I care about the kind (type) of error (business logic). b) I only care about errors in general (except programming errors). c) I care about any kind of error (framework).
n
Okay. At any rate, swift's approach is basically equivalent to returning Outcome<T>'s, but with syntactic sugar (which helps a lot)
Rust's ? is pretty nice too
m
Okay. At any rate, swift’s approach is basically equivalent to returning Outcome<T>’s, but with syntactic sugar (which helps a lot)
No
T
in Swift. Just
Outcome
.
But yeah, that’s what I’m missing in Kotlin.
Nevermind, I meant error type
n
The Result<T> we can't use that's in the standard library is basically a decent class. but without syntactic sugar, it's pretty painful.
foo().getOr { return it}
is way worse than
foo()?
. Maybe if they could adapt some of the null syntactic sugar for a built in Result type đŸ€·
m
I’d prefer not to re-use null checks for error checks.
A compromise like
try?
and
try!
in Swift is okay though.
Downside is that Swift’s keyword is very chain-unfriendly.
n
Yeah. It looks ok for one value but if you're really combining a lot of stuff it's not great
I mean
getOr { return it }
isn't that bad
m
It’s okayish. I had that as
ifError { return it }
n
Another option would be to simply allow inline functions to return from their caller scope, if such a thing is possible
m
problem is that you cannot use
return
in various situations
n
yeah, that's true. It's a bit tricky.
I personally tend to agree, that error handling is easily worthwhile enough to expend language design real estate on
it's one of the most worthwhile things for this purpose IMHO
m
Handling errors is at the core of programming. Currently solutions are simply half-hearted, especially for that importance.
💯 2
a
If you have a hundreds of functions each calling several other functions, every single call result would have to be unwrapped, errors returned and values be used for the remainder of the function.
@Marc Knaup Can you give an example about what you mean here? Just trying to understand how it went bad for you. Not asking for a hundred-function sample, just enough for us to understand that it’s not gonna scale very well.
j
Well when i try to load separate things which return all a Flow<Result<T>> and try to combine those flows into 1 its not super straightforward. Perhaps some more extension functions could solve some stuff there
n
I mean you can imagine pretty easily if all your functions are returning these Result<T>
then everything you are going to do is going to have these
getOr { return it }
j
T might differ ;) have to be combined in a final object
n
Copy code
fun foo() {
    val result = bar(baz1().getOr { return it }, baz2().getOr { return it }).getOr { return it }
}
You can see this is getting out of control
with exceptions this is just
Copy code
fun foo() {
    val result = bar(baz1(), baz2())
}
languages that have syntactic sugar for this, it might be something like:
Copy code
fun foo() {
    val result = bar(baz1()?, baz2()?)?
}
which isn't that bad
a
Ok, I see the problem. Thanks. I wouldn’t like the with the exceptions approach. It looks concise but it’s not explicit on what fails at all. But might work for your use-case where everything can seem to fail. Having language-baked syntactic sugar would be nice indeed.
101 Views