How do you guys typically work with error types? D...
# arrow
e
How do you guys typically work with error types? Do you try to model any/every possible error that can occur (like connection issues, etc), or just those related to the domain?
j
My current approach is to model anything that I need to handle explicitly. That means ALL domain errors, and SOME infrastructure errors. Unless I want to do anything with a connection error, I just let the exception be handled by a global exception handler (i.e. to return a 500 response). On the other hand, if I wanted to retry the operation I'd make the connection error an explicit error type.
Here's a nice post about the topic: https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/ Notice the classification of errors under "When should you use Result?".
gratitude thank you 2
❤️ 1
e
Thx!
c
This has been discussed a few times, overall: • it is more common for backend developers to use exceptions for infrastructure problems • it is more common for frontend developers to use error types for infrastructure problems
💯 1
s
What @CLOVIS said 😄
Do you try to model any/every possible error that can occur (like connection issues, etc), or just those related to the domain?
Only those related to the domain.
UserAlreadyExists
(unique violations),
InvalidPassword
, validations from incoming request, etc. https://github.com/nomisRev/ktor-arrow-example/blob/main/src/main/kotlin/io/github/nomisrev/DomainError.kt
👍 2
b
@simon.vergauwen How do you handle the errors as they continue to grow? Do you break out the
DomainError
into resource specific groups (or something else) or do you just keep growing the
DomainError
file?
s
I had a conversation with @CLOVIS recently, https://kotlinlang.slack.com/archives/C5UPMM0A0/p1701451925034189 it depends ... 😅 If the project becomes "too large" (a bit subjective) I would break out of
DomainError.kt
and split it more into logical groups. Wether that is resource specific, or domain specific, use-case, etc is also a bit subjective. I did already split it in my example project, but they're still defined in the same file. The hierarchy is defined as a tree, so it already has sub hierarchies which could be moved into their own files.
j
Why do you need a single hierarchy?
s
Convenience, otherwise you'd need union types. With a common parent, it's easy to combine all errors together at the end of the world. Alternatively, you need to map the errors.
Copy code
sealed class Parent

sealed class SubA : Parent
object ErrorA : SubA

sealed class SubB : Parent
object ErrorB: SubB

fun a(): Either<ErrorA, Int> = TODO()
fun b(): Either<ErrorB, Int> = TODO()

either<Parent, Int> {
  a() + b()
}
Otherwise:
Copy code
object OtherError

sealed class SubA
object ErrorA : SubA

sealed class SubB
object ErrorB: SubB

fun a(): Either<ErrorA, Int> = TODO()
fun b(): Either<ErrorB, Int> = TODO()

either<OtherError, Int> {
  val a = recover({ a() }) { OtherError }
  val b = recover({ b() }) { OtherError }
  a + b
}
c
More specifically, it's convenient because I represent infrastructure errors as error types. Because it's a single hierarchy, I can have a generic “display an error” UI component that has special cases for infrastructure errors. Note that I consider authentication/authorization issues infrastructure errors, and thus I map them as well. If you don't really have error types that are used in all your services, you can probably simplify quite a bit and maybe avoid the single hierarchy?
I want to write a Ktor wrapper that allows to
raise(404)
so you can
mapErrors
from your domain to the HTTP API (and back on the other side). I had a prototype of it last year but I wasn't happy with it.