Let's say you propagate errors using the `Either` ...
# server
d
Let's say you propagate errors using the
Either
(or
Result
or whatever) pattern. How do you combine errors from different services/methods to a new one? Example:
Copy code
fun callTwoServices(): Either<???, String> {
  val resultA: Either<ErrorA, Int> = serviceA.call()
  val resultB: Either<ErrorB, Int> = serviceB.call()
  // do something with the results, possibly propagating the errors
  ...
  return Either.Right(myResult)
}
The question is what should be the ??? type? There are two approaches I can think of • Create a sealed class
MyError
with subclasses
MyErrorA
and
MyErrorB
, and map the original
Error*
to new ones. The resulting error type would then be
MyError
. • Create
Error
sealed class already for the
Error*
subclasses and use this
Error
at all left positions. Neither approach seems ideal to me. While the first one looks more correct, it's quite boilerplaty. The second one then quickly leads to a god object (containing unrelated error types). I guess the ideal solution would be to use union type
Either<MyErrorA | MyErrorB, String>
. But since this feature is not in Kotlin yet what is the approach you take in this situation?
Just please note that while I'm using Arrow in this example it's just for illustration purposes. because this kind of question IMO arises when using any implementation of the Result pattern.
Cross-posting to #arrow after all 🙂
e
depends on the situation. in general I would try to wrap the errors into something that could make sense for downstream consumers, e.g.
CartError
regardless of whether the cause was a
ItemUnavailableError
or a
PaymentMethodError
.
d
So basically you would try to have a single error type per method? What if you wanted to propagate the distinction to consumers? E.g. when the downstream errors are
NotFound
and
Unauthorized
.
e
in practice I use a single (or a couple at most) parent error type per module
with sealed subtypes if downstreams should be able to act upon the difference
doesn't mean a subtype per upstream
d
So in the context of a microservice (typically a single module), you would have a single error hierarchy like https://github.com/nomisRev/ktor-arrow-example/blob/main/src/main/kotlin/io/github/nomisrev/DomainError.kt?
e
with the microservices I work on, it doesn't matter since it's all sent via JSON anyway…
d
True, but I assume you have several layers in the microservice and all these just pass the errors to the higher levels until they ultimately reach the controller where you finally decide what to do with them.
e
yeah. but since the type hierarchy isn't exposed outside the microservice, any way it's set up works
d
So quoting my original example, you would simply do
Copy code
sealed class DomainError {
  data object ErrorA: DomainError
  data object ErrorB: DomainError
  ...
}
? Multiple different errors might enable to you specify the same HTTP status (e.g. 404) with a specific error message.
👍 1
e
in our case, the cause is carried along as a property, so the error message will contain it even without specific subtypes. but sure
a
Yes, what I do is have each service use a single sealed class as the error type. Then I use a when expression at the http layer to convert it to the response.
r
I did something similar recently, but used sealed interfaces rather than sealed classes so that the return types are still limited to only the expected errors for a particular method, not the entire set of domain errors.
d
@ross_a Yes, that's basically my first option and I find that incredibly tedious and verbose because then you need a specific sealed class/interface for each method, possibly even different classes per service layer (eg. persistence vs business logic).
r
Yep that's what I saw. Would love union types but that's the closest I could get.
t
Unions would be amazing 🙂 (that would also kinda eliminate the need for an either, since the return type could just be Int | MyErrorA | MyErrorB. Sealed interfaces is the closest thing we have (both are ways to define a SUM type)
e
there's a difference between a discriminated/tagged union and an untagged union. not sure which you mean by
Int | MyErrorA | MyErrorB
but both lead to some tough design decisions when it comes to how it should work with the rest of the language and runtime…