kittinunf / Result

The modelling for success/failure of operations in Kotlin and KMM (Kotlin Multiplatform Mobile)

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Dealing with unexpected exceptions more elegantly

nieldw opened this issue · comments

Whenever an unexpected exception gets thrown while evaluating a function with Result it leads to an unintuitive ClassCastException. Here is a simple example:

import com.github.kittinunf.result.Result
import org.junit.jupiter.api.Test
import kotlin.test.fail

internal class ResultExceptionClassCastTest {
    class MyException: Exception()

    @Test
    fun `Unexpected exception leads to unintuitive ClassCastException`() {
        Result.of<String, MyException> {
            operation("something")
        }.fold({}, {
            fail("Not reached")
        })
    }

    private fun operation(it: String): String {
        throw IllegalArgumentException()
        return "never reached $it"
    }
}

Is there something that I as a library user can do to handle this better? Is this an issue with the library?

The simple solution would be to expect an Exception (or Throwable!), and then to cast to the expected type, if it indeed is that type, but this doesn't seem like a very good solution.

Hi there! At Fuel we do wrap all errors into FuelError, which is a boxed error type. Then we expect a result of _, FuelError. This allows us to quickly see if there is an error we didn't wrap (ClassCastException). The error handling code rethrows ClassCastException with stacktrace, so we know where it happened.

The FuelError is unwrapped (.cause)

I think there is very little we can do for this. Because in terms of Result, Result<T, E1> and Result<T, E2> are different types and represent different things. Unless you mark it as the top level type Result<T, Exception>.

Out of the top of my head, I think one possibility that I could make for this project is to relax the Error type of Result to be not dependent on the Exception or Throwable itself. Like, Result<T, E>, so that one can use the sealed class to represent Error like.

sealed class FindUserError {
    data class NotFound(val name: String) : FindUserError()
    data class MalformedName(val name: String) : FindUserError()
    data class FoundButHasBeenDeleted(val name: String) : FindUserError()
}

then one can use Result like

interface Repository {
  fun getUser(): Result<User, FindUserError> 
}

//usage
val result = dataSource.getUser()
when (result) {
  is Result.Failure -> {
    val error = result.error //FindUserError type
    //check each error type for appropriate UI display ...
  }
}

On the other hand, this looks like it moves Result to be something similar to Either type where there is already community support for this aka. Arrow. Worth to have another implementation or not, it probably depends on how one looks.

@kittinunf What you are mentioning here in your last comment is basically how I implemented Result in our application. It seems to me that it fits better to railway oriented programming because that is exactly what is being done there. I would be in favour of such a change.

Also arrow has a Try object which is quite similar: https://arrow-kt.io/docs/arrow/core/try but I feel no matter what, there would still be value in having Result as it is defined here, as imo it seems to me more specific and practical. In that way it makes much more sense to me compared to how e.g. arrow works.

@Globegitter Thanks for your comment, really appreciated. To be honest, I would love to hear whether this needs to be changed or not by the clients (developers) that uses this library if there is one 😄. I am open to change though.

@kittinunf I actually started using your library now and I started doing this:

typealias ResultFlow<T> = Flow<Result<T, ErrorMessage>>
typealias Result<T> = Result<T, ErrorMessage>
typealias Success<T> = Result.Success<T>
typealias Failure = Result.Failure<ErrorMessage>

sealed class ErrorClass : Exception("")
...

then it actually worked immediately with the code :) Having said that I would still prefer if the sealed class would not have to be a child of Exception just for the sake of it.

As a side note and bit off - topic, our API has started to center on Flows now as they are becoming stable and I built operators on top of ResultFlow, which I would be happy to contribute back here if there is interest.

Wow, on another side note, I also just realised there are loads of Result libraries out there - have you seen some of those yet?

I found:

Did not know this pattern was so popular for Kotlin. But the reason I am posting this here is as I also found it interesting to see how others have decided to implement the error handling.

With @Globegitter's help on this PR, I think we are able to support this. I think this conversation here might look a bit too stale. I would love to close this. Thanks everyone on this thread here :)