https://kotlinlang.org logo
Title
l

Lukáš Kúšik

08/12/2022, 10:31 AM
Hi! I am trying to model the error handling in our Android client. I have been inspired by this video

(1) Building applications with Kotlin and Arrow.kt in style - YouTube

, although I would like to apply these principles to the client side in this case. Considering this hierarchy:
sealed interface Error
sealed interface ApiError : Error {
    object NetworkError : ApiError
    object Unauthorized : ApiError
    object ServerError : ApiError
}
sealed interface DomainError : Error
sealed interface UserError : DomainError {
    object AlreadyExists : UserError
}
sealed interface BookError : DomainError {
    object AlreadyReserved : UserError
}

class RemoteApi {
    fun createUser(): Either<ApiError, UserDTO>
}
class Repository {
    fun createUser(): Either<UserError, UserDTO> (?) {
        val response: Either<ApiError, UserDTO> = remoteApi.createUser()
        // ... map known errors to UserError and return
    }
}
Let's say that there's a
Repository
which uses a
RemoteApi
and I'd like to create a new user on the backend. The
RemoteApi
does the POST call using Ktor, and returns the result of the
Either<ApiError, UserDTO>
type, where
ApiError
encapsules any network exceptions, internal server errors, etc. Now, I would like to parse expected errors (like
UserAlreadyExists
) as
UserError
of the common
DomainError
sealed type (similar to the video). I'd like the
Repository
to get the`Either<ApiError, UserDTO>` from the
RemoteApi
, process the known errors and return something like the
Either<UserError, UserDTO>
type. The problem with
Either<UserError, UserDTO>
is, that when a network exception occurs, it is an
ApiError
and not a
UserError
. The
Repository
would have to return something like
Either<Either<ApiError, UserError>, UserDTO>
which looks weird, or? You could return
Either<Error, UserDTO>
to contain both
ApiError
and
UserError
, but then you would also allow the
Error
to be a
BookError
, which would get ugly in when statements. Maybe using
Either<Error<UserError>, UserDTO>
would be the best, but I'm having trouble modelling the data types like that. Does anyone know of an elegant solutions for this, or this problem of modelling errors in repositories overall? Thank you.
s

stojan

08/12/2022, 11:08 AM
if I understood the problem correctly.... the issue you have is going from
Either<ApiError, A>
to
Either<UserError, A>
you can use
mapLeft
to transform from
ApiError
to
UserError
l

Lukáš Kúšik

08/12/2022, 11:10 AM
That was the first thing I tried, however, I then realized that the
ApiError
cannot really be mapped to a
UserError
(what `UserError`should be returned when the network fails?), so you still kind of need to somehow keep the original
ApiError
.
s

stojan

08/12/2022, 11:14 AM
maybe
DomainError : ApiError
instead of
Error
that way you are adding to it
l

Lukáš Kúšik

08/12/2022, 11:29 AM
This inheritance does not quite work, as you cannot upcast an existing
ApiError
to a
UserError
.
val response: Either<ApiError, UserDTO> = serverApi.createUser(user)

// Map to domain error
val result: Either<UserError, UserDTO> = response.mapLeft { e ->
  if (e is ApiError.Unexpected && e.statusCode == HttpStatusCode.Conflict) {
    UserError.AlreadyExists // UserError
  } else { 
    e // ApiError
  }   
} // Resolves to Either<ApiError, UserDTO> not Either<UserError, UserDTO>
UserError
would have to have an
UserError.ApiError
subclass or something similar that could be used to get a
UserError
here.
Using composition instead of inheritance would probably work, but I'm wondering if there is something better.
sealed class DomainError(val originalError: ApiError) : Error
s

stojan

08/12/2022, 11:33 AM
unfortunately Kotlin doesn't have untaged union types, that would be the best solution here https://youtrack.jetbrains.com/issue/KT-13108/Denotable-union-and-intersection-types
l

Lukáš Kúšik

08/12/2022, 11:38 AM
Thanks for the link, looks like that would be the proper solution. I even found someone there facing the same problem, and he seems to inline the
ApiError
cases in each
DomainError
subclass, having lots of duplicates. https://youtrack.jetbrains.com/issue/KT-13108/Denotable-union-and-intersection-types#focus=Comments-27-4497857.0-0
s

simon.vergauwen

08/12/2022, 12:48 PM
Ye... union types would really be the cherry on the pie for this. Hope we can get them soon-ish.
o

okarm

08/14/2022, 8:44 PM
I suspect that the composition approach is as elegant as you can get in Kotlin at this point in time. This is what I tend to do:
sealed class UserError {
  object E1 : UserError()
  object E2 : UserError()
  ...
  // Encapsulates the transport layer ApiErrors
  // without polluting the top level UserError API
  class UnmappableError(val cause: ApiError)
}
l

Lukáš Kúšik

08/15/2022, 7:07 AM
what do you guys think about this creation?
sealed interface UserError : DomainError {
    object E1 : UserError
    object E2 : UserError

    @JvmInline
    value class ApiError(private val base: ApiError) : UserError, ApiError by base  
}