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

Rafa Gómez

10/27/2023, 5:07 PM
Hi folks, we've been using arrow on our backend (kotlin and spring boot using DDD and Hexagonal Architecture) for almost 3 years now with great success. Lately we've been discussing how we model our use case errors and what should/should not be in there. We model all the possible errors of our use cases like this:
Copy code
sealed class BookAgendaError {
    data object InvalidIdentifier : BookAgendaError()
    data object AgendaNotFound : BookAgendaError()
    data object MaxCapacityReached : BookAgendaError()
    data object PlayerAlreadyBooked : BookAgendaError()
    data object AvailableHourNotFound : BookAgendaError()
    class Unknown(val cause: Throwable) : BookAgendaError()
}
This way every possible error inside the use case is modeled and in the event of something unexpected were to happen on our infra (repos, publishers, 3rd parties) we catch the exception (using Either.catch) and short circuit until we reach the end of the workflow (normally a controller/subscriber) and then we handle it. Most of the times would look something like this
Copy code
private fun BookAgendaError.toServerError() =
        when (this) {
            is MaxCapacityReached -> Response.status(CONFLICT).withBody(MAX_CAPACITY_REACHED)
            is PlayerAlreadyBooked -> Response.status(CONFLICT).withBody(USER_ALREADY_BOOKED)
            is AgendaNotFound -> Response.status(NOT_FOUND).withBody(AGENDA_DOES_NOT_EXIST)
            is AvailableHourNotFound -> Response.status(NOT_FOUND).withBody(AVAILABLE_HOUR_DOES_NOT_EXIST)
            is InvalidIdentifier -> Response.status(BAD_REQUEST).withBody(INVALID_IDENTIFIERS)
            is Unknown -> throw cause
        }
We end up throwing the root cause so our observality platform is aware of the root problem and our generic exception handler provides the default 500. My main question is, whenever you model use case errors do you find useful to have the "infra" error as a part of that model? Personally I find it easier to reason about what happens if some unexpected error appears but I'm enjoying avoiding it since it's not something you may be able to recover from and adds some boilerplate. How does everyone here model these situations? Any thoughts are welcome
👀 2
r

raulraja

10/27/2023, 5:21 PM
I follow an approach similar to yours but as a rule of thumb I don't try to catch errors and turn them into
Unknown
. I follow that if the error is something you constructed and raised yourself then you handle it otherwise just bubbles up. If you just turn exception into Unknown and don't do anything with them but rethrowing I'm not sure I see the point of it. Implicitly all functions in Kotlin can throw unchecked exceptions, so even having
Unknown
there does not make the function total.
👍 1
r

Rafa Gómez

10/27/2023, 5:26 PM
that's something my team has been thinking as of late, focus only on the business errors regarding use cases and simply avoid the overhead of the things than can fail in infra.
🔝 2
thanks for the quick feedback
👍 1
s

simon.vergauwen

10/27/2023, 5:57 PM
y

Youssef Shoaib [MOD]

10/27/2023, 7:52 PM
Minor nitpick: I'd recommend using a sealed interface instead of a sealed class in almost all cases. It allows you to have more complicated hierarchies later on.
🔝 1
r

Rafa Gómez

10/27/2023, 8:07 PM
We use them sometimes but since all of our error models represent a single use case and our workflows are made very atomic we're never in need for hierarchies. But thanks for the tip
👌 1
m

mitch

10/27/2023, 9:11 PM
hello! @Rafa Gómez we had very similar setup too using Spring Webflux. Like others, I'd definitely recommend just bubble up exceptions. Running this setup in prod for ~5 years. From experience: • use sealed hierarchy to model your domain logic error boundaries. • If there's any unknown ex, let that bubble up and hand that over to your global error handler to translate that into a correct response and fire the correct observability trace / metrics. (tangent similarly to your setup we've installed a custom coroutine webfilter, Spring stock standard runs it in a different coroutine context which broke our observability). Adapting to your example we'd only have:
Copy code
sealed class BookAgendaError {
    data object InvalidIdentifier : BookAgendaError()
    data object AgendaNotFound : BookAgendaError()
    data object MaxCapacityReached : BookAgendaError()
    data object PlayerAlreadyBooked : BookAgendaError()
    data object AvailableHourNotFound : BookAgendaError()
}
Don't "catch'em all" or don't use the "pokemon exception handling" interesting term we use in house. When there's unknown exception, just let that explode. The reason why it's best to avoid
runCatching
in Spring (& Kotlin in general) is we can also catch unrelated exceptions such as cancellations, netty's aborted exception, OOM, etc. If we really need to catch exceptions (e.g. due to use of AWS lib or java libs), we use Arrow's
Either.catch { }
and
Option.catch { }
which already do the right things (i.e. they catch only non fatal error, and let cancellations bubble up). (On another note, perhaps just fyi. we also try to avoid using nulls in our logic because we interact with reactive libraries. Also running webflux under the hood, we want to avoid propagating nulls down and creating empty monos. Also we've been bitten a couple of times by nested nullability bugs that is quite honestly pretty evil to debug)
🔝 2
💯 1
r

Rafa Gómez

10/27/2023, 10:03 PM
We're using a very similar approach indeed @mitch. Thanks for the feedback, we're definitely going to avoid adding all the boilerplate to handle errors on ports (except some very specific situations).
🙌 1
s

simon.vergauwen

10/28/2023, 10:29 AM
@mitch you should really give a talk, or write couple blogpost about this 😉 Couldn't agree more with all the other feedback given here!
c

CLOVIS

10/28/2023, 11:08 AM
Interesting. I'm currently going the opposite way (I have
ConnectionLost
and
UnknownError
as part of my error hierarchies) mostly to differentiate between "an exception was thrown" (programming error) and "the server failed with an error I didn't recognize" (should be recovered). This is mostly motivated by UI frameworks generally reacting very badly to exceptions, and I don't want to have two error handling strategies in all components (both catching errors and displaying failed eithers).
It also means that the UI layer doesn't have to know about the possible exceptions thrown by the API layer, since they're communicated as a failed error from the domain layer.
s

simon.vergauwen

10/28/2023, 11:29 AM
Right, to be fair I think for a UI application it's a bit different. On a server a truly exceptional thing results in
500
, and allowing it to bubble up as an exception is thus fine but in a UI application this crashes the app. Which is not the case for the server. This is also kind-of why
fold
for
Raise
covers the
Throwable
case, such that you can still deal with them as
UnkownError
without having to model it in that way. I was personally a big fan of using streaming UIs though, and it seems with Compose this has become vastly nicer. Using Molecule for example, and working with
Either
is also much nicer in that way:
Copy code
@Composable
fun ProfilePresenter(
  userFlow: Flow<Either<Error, User>>,
  balanceFlow: Flow<Either<Error, Long>>,
): Either<Error, ProfileModel> {
  val user by userFlow.collectAsState(null)
  val balance by balanceFlow.collectAsState(0L)

  return either {
    val u = user.bind()
    if (u == null) Loading
    else Data(user.name, balance)
  }
}
Although I haven't used this in an actual UI application, this should work pretty similar to what I was doing before. Dealing with
UnkownError
can then become:
Copy code
moleculeFlow(mode = Immediate) {
  ProfilePresenter(userFlow, balanceFlow)
}.catch { e -> SomethingWentWrong(e) }
🔥 1
👁️ 1
c

CLOVIS

10/28/2023, 2:10 PM
> Right, to be fair I think for a UI application it's a bit different. On a server a truly exceptional thing results in
500
, and allowing it to bubble up as an exception is thus fine but in a UI application this crashes the app. Which is not the case for the server. I share my domain (and thus failure cases) between client and server 🙂 > I was personally a big fan of using streaming UIs though, and it seems with Compose this has become vastly nicer. I honestly love this about the Kotlin ecosystem. If you squint hard enough: • Coroutines: write asynchronous stuff as if it was imperative • Arrow: handle errors as if they were imperative exceptions • Compose: write streaming UIs as if they were imperative
💯 1
4 Views