The question is about how to model errors in the R...
# getting-started
d
The question is about how to model errors in the Result pattern (irrespective of the concrete implementation). Specifically, how to translate the errors on the API (uppermost) layer. Details in 🧵
Let's say I have two services/domains X, Y with overlapping error types. The services use the Result pattern but for simplicity I implement here only the error part. In the controller (main) the generic errors are transformed to API errors (implemented here as just by throwing an exception). The current implementation looks like this:
Copy code
interface ApiError {
    fun toApi(): Nothing
}

sealed interface DomainXError : ApiError
sealed interface DomainYError : ApiError

data object UnknownError : DomainXError {
    override fun toApi() = error(HttpStatus.UNPROCESSABLE_ENTITY.value())
}

data object NotFound : DomainXError, DomainYError {
    override fun toApi() = error(HttpStatus.NOT_FOUND.value())
}

data object Unauthorized : DomainYError {
    override fun toApi() = error(HttpStatus.UNAUTHORIZED.value())
}

fun callServiceX(): DomainXError = UnknownError
fun callServiceY(): DomainYError = NotFound

fun main() {
    val resultX = callServiceX()
    resultX.toApi()
}
While this works, I would like to restrict the
toApi
calls only to the controller. The dispatch/context receivers seem a natural solution to this problem but I run into issues. TBC
I tried doing this:
Copy code
interface ApiScope<T> {
    fun T.toApi(): ApplicationException
}

object UnknownErrorScope : ApiScope<UnknownError> {
    override fun UnknownError.toApi() = error(HttpStatus.UNPROCESSABLE_ENTITY.value())
}

// similarly for the other scopes

val domainXErrorScope = object : ApiScope<DomainXError> {
    override fun DomainXError.toApi() =
        when (this) {
            is UnknownError -> this.toApi()
            is NotFound -> this.toApi()
        }
}

fun main() = with(domainXErrorScope) {
    val resultX = callServiceX()
    resultX.toApi()
}
But I'm unable to delegate to the concrete
toApi
implementations in the
DomainXError.toApi
. Is there a way out?
The question basically is if there is way how to provide specific scope in each of the branches in
Copy code
when (this) {
            is UnknownError -> this.toApi() // I need UnknownErrorScope here
            is NotFound -> this.toApi()
        }
I found a way but it's not very nice because it just gathers all the API error inside a single god object which is exactly what I wanted to avoid with the error-type-per-domain design 🤷
Copy code
val apiScope = object : ApiScope<ApiError> {
    override fun ApiError.toApi() =
        when (this) {
            is UnknownError -> error(HttpStatus.UNPROCESSABLE_ENTITY.value())
            is NotFound -> error(HttpStatus.NOT_FOUND.value())
            is Unauthorized -> error(HttpStatus.UNAUTHORIZED.value())
        }
}

fun main() = with(apiScope) {
    val resultX = callServiceX()
    resultX.toApi()
}
d
This works:
Copy code
interface ApiError {
    fun ApiController.toApi(): Nothing
}

sealed interface DomainXError : ApiError
sealed interface DomainYError : ApiError

data object UnknownError : DomainXError {
    override fun ApiController.toApi() = error("Unknown error")
}

data object NotFound : DomainXError, DomainYError {
    override fun ApiController.toApi() = error("Not found")
}

data object Unauthorized : DomainYError {
    override fun ApiController.toApi() = error("Unauthorized")
}

fun callServiceX(): DomainXError = UnknownError
fun callServiceY(): DomainYError = NotFound

class ApiController {
    fun doSomething() {
        with(callServiceX()) { toApi() }
    }
}
The trick is the
with
syntax, to make the ApiError instance an implicit receiver. You could also use
callServiceX().run { toApi() }
d
The problem with this is that
ApiController
is again a god object. What if I have (and typically I do) more controllers? Let's say ControllerA handles (or combines) domains X,Y, while ControllerB handles domain Z. Would I need to to write
Copy code
interface ApiError {
    fun ControllerA.toApi(): Nothing
    fun ControllerB.toApi(): Nothing
}
? Perhaps that's unavoidable given the lack of union types. In fact even the definition of Domain*Error unions is kind of inverted...
d
I think the idea of only allowing
toApi
to be called from certain contexts is flawed to begin with.