Fred Friis
03/14/2023, 1:45 PMSam
03/14/2023, 1:57 PMResult
only has one generic parameter, where Either
has two. That means that Either
can model everything that Result
can, but Result
cannot model everything that Either
can. If you need type information about your errors, Result
will not help you.Fred Friis
03/14/2023, 1:58 PMThe first and foremost use of exceptions in Kotlin is to handle program logic errors. Exceptions are used to check preconditions and invariants in code that static type system cannot track down during compilation. For example, if you a have a function that updates order quantity that must be always positive by the business logic of the code, then you might write something like:
/** Updates order [quanity], must be positive. */
fun updateOrderQuanity(orderId: OrderId, quantity: Int) {
require(quantity > 0) { "Quantity must be positive" }
// proceed with update
}
Calling this function with a negative or zero quantity indicates a logic error in some other place in the code and throws an exception. The same happens when you try to get an element by an out of range index from a list, divide by zero, etc.
(https://elizarov.medium.com/kotlin-and-exceptions-8062f589d07)Fred Friis
03/14/2023, 2:06 PMSam
03/14/2023, 2:08 PMEither
going anywhere. It has many use cases that aren’t addressed by the Result
type.Fred Friis
03/14/2023, 2:11 PMSam
03/14/2023, 2:17 PMEither
then my Left
type is typically some very descriptive sealed type that is not a Throwable
. The use case for Result
, where you want to use a Throwable
but not throw it, is in my experience a fairly small intersection. It comes up occasionally in framework code, where you want to shuttle an exception from one place to another. This is actually what it was designed for (specifically for the behind-the-scenes implementation of coroutines). But in my experience it isn’t typically relevant for day-to-day code.Fred Friis
03/14/2023, 2:25 PM@GET
fun getUser(id: Int) : User {
return userService
.getUser(id) //Either<GetUserError, User>
.fold({200(it)}, {when UserNotFound 404 etc etc map other errors to correct status code})
}
this is my understanding of an example of idiomatic use of Eitherpakoito
03/14/2023, 2:29 PMpakoito
03/14/2023, 2:29 PMpakoito
03/14/2023, 2:30 PMFred Friis
03/14/2023, 2:42 PMsimon.vergauwen
03/14/2023, 2:53 PMimo, exceptions should be just that, exceptional💯 this
CLOVIS
03/14/2023, 3:13 PMResult
is really not great. runCatching
catches CancellationException
and breaks coroutines, and as the others have said it's untyped.Fred Friis
03/14/2023, 3:17 PMSam
03/14/2023, 3:21 PMCLOVIS
03/14/2023, 3:21 PMCancellationException
is used internally by coroutines. If you don't know exactly what you're doing, you should not touch it (not catch it, not rethrow it…). Having a catch(e: Exception)
or catch(e: Throwable)
in your code can create memory leaks or break coroutines in other ways.
Arrow (at least, the newer versions) also use CancellationException
internally, and risk being broken too.
runCatching
has a catch (e: Throwable)
, and should therefore not be used in coroutines.
Either.catch
is clever and doesn't have this problem (safe to use with coroutines).Fred Friis
03/14/2023, 3:23 PMCLOVIS
03/14/2023, 3:24 PMCLOVIS
03/14/2023, 3:26 PMcatch (e: Throwable)
or catch (e: Exception)
has a very bad smell. But then again, it already has a bad smell in regular code.
As long as you always catch specifically the type of exception you expect, or use clever functions like Either.catch
, you don't have to think about it.CLOVIS
03/14/2023, 3:31 PMFred Friis
03/14/2023, 3:34 PMCLOVIS
03/14/2023, 3:34 PMFred Friis
03/14/2023, 3:34 PMCLOVIS
03/14/2023, 3:37 PMSam
03/14/2023, 3:38 PMeffect
scopes that allow you to exit early without a return, hence exceptions are required for correct control flow. Any code that catches broad categories of throwables risks catching these control flow exceptions; this is an unavoidable trade-off of the functionality.CLOVIS
03/14/2023, 3:40 PMCLOVIS
03/14/2023, 3:41 PMrunCatching
's documentation, but they haven't replied yet.Fred Friis
03/14/2023, 3:42 PMFred Friis
03/14/2023, 3:43 PMrunCatching
or try catch (all exceptions)
, is that enough to make this a non issue or..?CLOVIS
03/14/2023, 3:44 PMCancellationException
without knowing exactly what you're doing, you can break Coroutines and Arrow. Coroutines and Arrow themselves are written by people who know what they are doing, and are very careful to use it correctly.Fred Friis
03/14/2023, 3:44 PMCLOVIS
03/14/2023, 3:45 PMCancellationException
, you have nothing to worry about, yes.pakoito
03/14/2023, 3:45 PMpakoito
03/14/2023, 3:45 PMnonFatalOrThrow
is peppered through code that interacts with coroutinespakoito
03/14/2023, 3:46 PMCLOVIS
03/14/2023, 3:47 PMNonFatal
should have been a part of Coroutines itself. It's great that Arrow makes it so easy though 🙂Fred Friis
03/14/2023, 3:48 PMCLOVIS
03/14/2023, 3:49 PMCLOVIS
03/14/2023, 3:50 PMFred Friis
03/14/2023, 3:53 PMSam
03/14/2023, 3:54 PMCLOVIS
03/14/2023, 3:55 PMCLOVIS
03/14/2023, 3:56 PMFred Friis
04/26/2023, 3:12 PMinside a coroutine, yeah you need to be careful if you are using the cancellation mechanism, but we're not really doing that. we're not generally launching our own coroutines. ktor provides us the coroutine per request, so we can easily operate within that. where we use run catching is to trap a database error for example.I'm simply not familiar enough with it all to know if the above is fine or whether it could potentially result in the issues discussed in the thread (even then, arguably, the potential for issues is pretty small, no? especially if the machine isn't struggling, then there would be little reason for the parents/scheduler to kill children coroutines etc)
CLOVIS
04/26/2023, 3:28 PMrunCatching
, when used incorrectly (and if you're using it for error management, you are using it incorrectly) breaks coroutines and every other exception-based library. It doesn't matter if it's inside Ktor or anywhere else.CLOVIS
04/26/2023, 3:31 PMEither.catch
, a specific try…catch
with a specific exception type (do NOT catch Throwable, Exception or RuntimeException!), or just let it bubble up normally and use Ktor's StatusPages plugin to convert it into a proper HTTP error code.Fred Friis
04/26/2023, 3:37 PMCLOVIS
04/26/2023, 3:38 PMrunCatching
.CLOVIS
04/26/2023, 3:38 PMFred Friis
06/14/2023, 10:11 PMkotlin
fun Route.myEndpoints(
myClient: MyClient,
myDAO: MyDAO
) {
get("/my-endpoint") {
myClient
.makeACall()
.flatMap { foo ->
myDAO
.query(
"select * from foo where foo.id = :",
foo.id,
FooMapper
)
}.fold (
{ call.respond(HttpStatusCode.OK, it) },
{ call.respond(HttpStatusCode.InternalServerError) },
)
}
}
kotlin
class MyClient(private val client: HttpClient) {
suspend fun makeACall(): Result<Foo> = runCatching {
// imagine this takes 5 seconds
client
.post("some/other/endpoint") {
setBody("""{"foo":"bar"}""")
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
}.body()
}
}
kotlin
class MyDAO(private val ds: DataSource) {
suspend fun <T> query(
// arguments
): Either<DbError, T> = runCatching {
// imagine this takes 5 seconds
template.queryForObject(sql, args, mapper)
}
}
}
given a client calling my-endpoint (and this is but one of many calls to many endpoints in the app)
because runCatching catches Throwable, which is the superclass that contains both JVM Errors (which should never be caught by anything) and Exception
a) if the machine that our app is running on experienced some catastrophic JVM Error that is not recoverable or controllable by us, eg OutOfMemoryError, InternalError or StackOverflowError, it would be caught by the Result and we would happily go on pretending that that never happened (even though what should have happened is probably the instance should have died so it can be replaced by another by k8s or whatever)
b) imagine the client has a timeout of 2 seconds against our endpoint ie after 2 seconds they terminate the call and no longer wait for a response
note that our endpoint performs two operations sequentially, each taking 5 seconds for a total of 10 seconds before responding
we could perform them simultaneously using awaitAll and cut our response time down to 5 seconds, but it wouldn't matter - the client only waits for 2 seconds anyway before closing the connection
our Ktor endpoint, effectively the parent suspend fun, will detect that the client has closed the connection and is no longer listening, and send a cancel/interrupt to its child coroutine that it started with runCatching
but because runCatching catches Throwable ie including CancellationException, the child coroutine that it started with runCatching will keep running not only until it gets a response, it will potentially keep the parent and itself alive as zombie coroutines indefinitely (because the cancellation was generated but caught so now they're just hanging around)
eventually, given enough calls like this to my-endpoint, this will result in memory leaks and other unpredictable, hard to debug behavior for the instance running the appFred Friis
06/14/2023, 10:16 PMCLOVIS
06/15/2023, 7:48 AMResult
really isn't made for error management. IMO it shouldn't have been added to the standard library because people expect this to be correct, but it's very wrong.Fred Friis
06/15/2023, 12:41 PMCLOVIS
06/15/2023, 1:11 PM