https://kotlinlang.org logo
#arrow
Title
# arrow
y

Yannick Lazzari

07/15/2022, 3:54 PM
Hi all. Question regarding error handling inside an
effect { ... }
block. Given the following function:
Copy code
fun function1: Effect<ErrorAdt, String> = effect {
  val val1: String = function2()
  val val2: String = function3()
  return val1 + val2
}
and that both
function2
and
function3
are possibly throwing exceptions, what is the best way to convert any exceptions they might throw into my
ErrorAdt
? Should I simply wrap each call to the functions inside
try/catch
blocks and convert them there? Or is there another error handling function that I can call on the effect, where I can centralize all error mapping logic, using some pattern matching on any thrown exceptions? I can't seem to find any in the docs, but just curious. Thank you!
s

stojan

07/15/2022, 4:25 PM
You can use catch or catchAndFlatten from the Either companion object https://arrow-kt.io/docs/apidocs/arrow-core/arrow.core/-either/-companion/index.html#functions
☝️ 1
s

simon.vergauwen

07/16/2022, 1:07 PM
@stojan has suggested an API before, which I have been thinking about adding for
effect
because this is quite a common use-case when wrapping foreign APIs. https://github.com/arrow-kt/arrow/pull/2746 @Alejandro Serrano Mena proposed a new API recently which is closely related to this, but only works over typed errors
R
. I've been thinking how we can nicely add another API to complement this but for
Throwable
.
Copy code
effect<ErrorAdt, String> {
  Effect.attempt {
    function2()
  }.refineOrThrow<IllegalStateException>()
    .catch { illegalState ->
      // recover with value,
      // or shift ErrorAdt
    }
}
👍 1
Effect.attempt
here would return
Effect<Throwable, A>
Plan in Arrow 2.0 is to make
Effect
a regular class, which then can extend
suspend EffectScope<R>.() -> A
so we would automatically get the
catch
syntax as designed by @Alejandro Serrano Mena in the PR.
y

Yannick Lazzari

07/18/2022, 12:54 PM
Thanks for the info @simon.vergauwen! Nice to know it's being looked at. What I actually had in mind, was something more along the lines of:
Copy code
effect<ErrorAdt, String> {
  suspendingFunctionThatCanThrow1();
  suspendingFunctionThatCanThrow2();
}.handleThrowable { t ->
  // recover with value or shift to ErrorAdt
}
If either the first or second function throws an exception, the
handleThrowble
method is invoked, otherwise if the body of the effect block results in a value or the error ADT, that is returned instead. So more alongs the lines of what
handleError
and
handleErrorWith
do, but for throwables. Would that make sense? It looks a bit more succinct like that I find.
t

than_

07/18/2022, 2:23 PM
This grabbed my interest and based on code I shamelessly stole from Alejandro's PR 😄 , I came up with:
Copy code
context(EffectScope<ERR>)
@OptIn(ExperimentalTypeInference::class)
fun <ERR, A> attempt(@BuilderInference f: suspend () -> A): suspend EffectScope<ERR>.() -> A = { f() }


context(EffectScope<ERR>)
suspend inline infix fun <ERR, A> (suspend EffectScope<ERR>.() -> A).handleThrowable(
    crossinline recover: suspend (Throwable) -> A
): A =
    effect(this@handleThrowable).fold(
        error = { recover(it.nonFatalOrThrow()) },
        recover = { shift(it) },
        transform = ::identity
    )
seems to be working really nice:
Copy code
context(EffectScope<ApplicationError>)
suspend fun getChannel(radioCode: String): ChannelRes =
    attempt {
        service.getChannel(radioCode = radioCode)
    } handleThrowable {
        shift(...) // boring error domain mapping
    }
s

simon.vergauwen

07/25/2022, 12:40 PM
Suspending functions that can throw can easily be wrapped by
Result<A>
from Kotlin Std. Then you can achieve following syntax as well.
Copy code
effect<ErrorAdt, String>{
  val x = runCatching {
    suspendingFunctionThatCanThrow1() 
  }.bind { t -> ErrorAdt.First(t) }
  val y = runCatching {
    suspendingFunctionThatCanThrow2() 
  }.bind { t -> ErrorAdt.Second(t) }
  x + y
}
We're thinking of better APIs to do this, but it's hard to come up with a good API 🤔
Copy code
effect<ErrorAdt, A> {
  // no exceptions will be caught here
}.handleThrowable { t ->
   // throwable can still be resolved safely
}
y

Yannick Lazzari

07/25/2022, 1:21 PM
Thanks for the suggestions @simon.vergauwen! I'm curious, do you see something wrong with the
handleThrowable
function above, which I had also suggested earlier in the thread? In terms of API, it looks consistent with the other
handleError
and
handleErrorWith
functions that already exist for
Effect
. And definitely less verbose than the other suggestion above. There may be good reasons you'd still want to surround each function call with its own error handling code, because handling errors for each function is so different that it's best to separate it out like that, but I can also definitely see a lot of cases where you really don't care and simply want to shift on some error value for any throwable that's thrown, in which case having this
handleThrowable
definitely could be handy. I mean, people can easily write their own extension function that does something similar, but it would be useful to have something out of the box. Just a thought!
s

simon.vergauwen

07/25/2022, 1:23 PM
Yes, I've been thinking about this... You can implement a variation of the above function in a style that is closer to what you originally shared.
Copy code
fun <E, A> Effect<E, A>.handleThrowable(
  recover: suspend EffectScope<E>.(Throwable) -> A
): Effect<E, A> = effect {
  fold(
    { t -> recover(this@effect, t) },
    { e -> shift(e) },
    { a -> a }
  )
}
y

Yannick Lazzari

07/25/2022, 1:25 PM
Thanks again for the suggestion!
👍 1
s

simon.vergauwen

07/25/2022, 1:25 PM
The problem we're trying to answer I think in Arrow is which API do we want to expose? Do we want to expose all variations? Or perhaps just
Copy code
effect<E, A> {
  attempt {
    effectA()
  } catch { e: E ->
    A
  }.catchThrowable { t : Throwable ->
    A
  }
}.catch { e: E ->

}.catchThrowable { t : Throwable ->

}
y

Yannick Lazzari

07/25/2022, 1:31 PM
If
effectA()
returns an
Effect
already, and you have a function for handling errors on the
Effect
type directly, wouldn't this be more like:
Copy code
effect<E, A> {
  effectA().catch { e: E ->
    A
  }.catchThrowable { t : Throwable ->
    A
  }
}.catch { e: E ->

}.catchThrowable { t : Throwable ->

}
It doesn't seem like it would be necessary to wrap the call to
effectA()
with
attempt
, and even wonder what would be the point of
attempt
if you have all the error handling capabilities on the
Effect
type directly. When would you choose to still wrap an effect call with
attempt
as oppose to use the
catchXXX
or
handleXXX
functions?
s

simon.vergauwen

07/26/2022, 9:05 AM
The reason for
attempt
is that it allows you to run
Effect<E, A>
within
effect<E2, B>
. For example,
Copy code
fun effectA(): Effect<Int, Boolean> = effect { shift(-1) }

effect<String, Int> {
   val x = attempt { effectA().bind() } catch { i: Int -> i }
   x + 1
}
Even though
effect
here has error type
String
we can run an effect with error type
Int
within
attempt
as long as we resolve the error in
catch
or
shift
an error of type
String
within the
catch
lambda
So it allows for mixing, and combine effect with different errors in a convenient API
y

Yannick Lazzari

07/26/2022, 12:54 PM
Ah. Gotcha. Thanks!
7 Views