suspendCoroutine crashes app whenever Exception instance is created

Viewed 710

I have a suspend fun, which supposed to return Result.success when everything's good or Result.failure if an error occurs or if "isBlocked" flag is set.

override suspend fun userData(userId: String) = suspendCoroutine<ResultData> { continuation ->
    userReference(userId).get().addOnSuccessListener { snapshot ->
        val data = snapshot.data ?: mutableMapOf()
        replaceTimestampWithDate(data)

        if ((data["isBlocked"] as? Boolean) == true) {
            continuation.resume(Result.failure(Exception()))
        } else
            continuation.resume(Result.success(data))
    }.addOnFailureListener {
        continuation.resume(Result.failure(it))
    }
}

However, whenever the instance of Exception() is created, the app crashes.

java.lang.Exception
    at co.thoron.langmate.data.providers.DataProvider$userData$$inlined$suspendCoroutine$lambda$1$1$1.onSuccess(DataProvider.kt:72)
    at co.thoron.langmate.data.providers.DataProvider$userData$$inlined$suspendCoroutine$lambda$1$1$1.onSuccess(DataProvider.kt:29)
    at com.google.android.gms.tasks.zzn.run(com.google.android.gms:play-services-tasks@@17.1.0:4)
    at android.os.Handler.handleCallback(Handler.java:883)
    at android.os.Handler.dispatchMessage(Handler.java:100)
    at com.google.android.gms.internal.tasks.zzb.dispatchMessage(com.google.android.gms:play-services-tasks@@17.1.0:6)
    at android.os.Looper.loop(Looper.java:214)
    at android.app.ActivityThread.main(ActivityThread.java:7356)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

I don't understand why it's happening, since I'm not even throwing the Exception. I've tried wrapping it in try { ... } catch { ... }, contextCoroutine, but I keep getting the same result. I don't even want the coroutine to "see" the exception, I just want to pass it through the Result type.

2 Answers

As Tensky suggested, I ended up creating my own implementation of the Result type. Below is what I created. I'll try cleaning it up and adding more failure reasons later on, but for now it seems to be working nicely.

sealed class Result<T>(val value: T?, val failureReason: String?) {
    class Success<T>(val valueData: T) : Result<T>(valueData, null)
    sealed class Failure<T>(failureReason: String) : Result<T>(null, failureReason) {
        class Unknown<T>(val failureReasonData: String) : Failure<T>(failureReasonData)

        companion object {
            fun <T> unknown(failureReason: String) = Failure.Unknown<T>(failureReason)

            operator fun <T>invoke(exception: Throwable) = when (exception) {
                else -> Failure.Unknown<T>(exception.toString())
            }
        }
    }

    companion object {
        fun <T>success(value: T) = Success(value)
    }

    fun valueOrDefault(defaultValue: T) = value ?: defaultValue

    inline fun <R>map(transform: (value: T) -> R): Result<R> = when (this) {
        is Success -> Result.success(transform(valueData))
        is Failure.Unknown -> Failure.unknown(failureReasonData)
    }

    inline fun <R>mapResult(transform: (value: T) -> Result<R>): Result<R> = when (this) {
        is Success -> transform(valueData)
        is Failure.Unknown -> Failure.unknown(failureReasonData)
    }
}
Related