Hello Guys, I am facing a weird issue, and maybe s...
# coroutines
b
Hello Guys, I am facing a weird issue, and maybe some folks can help me here. I tried exception handling in coroutines, but when I ran the following code using
runBlocking
, it threw an exception even though it is surrounded by a
try-catch
block. I want to know how
runBlocking
works in this scenario and why this exception is thrown in
runBlocking
but not in a regular Android project.
Copy code
import kotlinx.coroutines.*

fun main() = runBlocking {
    val deferredUser = async {
        // Simulating an exception
        throw Exception("Something went wrong!")
    }

    try {
        deferredUser.await()
    } catch (ex: Exception) {
     
    }
}
However, when I tried a similar approach in my Android project, the exception is handled as expected:
Copy code
private fun tryAndTest() {
        viewModelScope.launch {
            val deferredUser = getUser()
            try {
                deferredUser.await()
            } catch (ex: Exception) {
                Log.d("viewModel", ex.toString())
            }
          
        }
    }

    private fun getUser() = viewModelScope.async {
        // Simulating an exception
        throw Exception("Something went wrong!")
    }
s
viewModelScope
from android's ViewModel is using a supervisorScope internally, which will catch those exceptions and not let it propagate further up https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:lifecycl[…]eViewModelScope&ss=androidx%2Fplatform%2Fframeworks%2Fsupport In your runBlocking example you do not have a supervisorScope() to do the same
j
Id say it differently. The supervisor scope doesn't get cancelled and therefore the exception handling code gets hit. With runblocking the parent is cancelled and never resumes to handle the exception
b
@Stylianos Gakis you mean
SupervisorJob
?
👍 1
s
SupervisorJob()
indeed, my bad. Exceptions inside async blocks is always tricky. I honestly don't know what the expected behavior is here along with runBlocking.
r
The following logs:
Copy code
Caught exception: java.lang.Exception: Something went wrong!
Caught exception 2: java.lang.Exception: Something went wrong!


import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking

try {
    runBlocking {
        val deferredUser = async {
            // Simulating an exception
            throw Exception("Something went wrong!")
        }

        try {
            deferredUser.await()
        } catch (ex: Exception) {
            println("Caught exception: $ex")
        }
    }
} catch (ex: Exception) {
    println("Caught exception 2: $ex")
}
the exception is first caught at the await, and then because the async was launched in the runBlocking scope, again at the end because I think it essentially does a
.join()
to ensure all child jobs have completed i.e. it is the equivalent of:
Copy code
try {
    runBlocking {
        val deferredUser = async {
            // Simulating an exception
            throw Exception("Something went wrong!")
        }

        try {
            deferredUser.await()
        } catch (ex: Exception) {
            println("Caught exception: $ex")
        }
        
        deferredUser.join()
    }
} catch (ex: Exception) {
    println("Caught exception 2: $ex")
}
b
I find this case really interesting, and I am still seeking an answer. If anyone discovers why
runBlocking
behaves differently, please let us know.
r
because
viewModelScope
has a
SupervisorJob
attached to it, you can make
runBlocking
behave the same way by attaching
SupervisorJob
to its scope
runBlocking(SupervisorJob()) {}
b
viewModelScope
has a
SupervisorJob
thats clear to me but is
runBlocking
is little confusing wrt exceptions
r
it throws once on the
.await()
which you are catching, and once again on the implicit
join()
at the end (which you are not, but which is suppressed if you are attaching
SupervisorJob
)
Untitled.kt
b
Thanks @ross_a I got other 2 behaviours Can you please explain this 2nd one
Copy code
runBlocking {
    val deferredUser = async(Job()) {
        // Simulating an exception
        throw Exception("Something went wrong!")
    }

    try {
        deferredUser.await()
    } catch (ex: Exception) {
        println("Caught exception: $ex")
    }
}
r
In that case the child is launched with its own detached
Job()
which doesn't have a reference to the parent to pass the exceptions back up Without specifying a Job it is implicitly created like this:
Copy code
runBlocking {
    val parentJob = coroutineContext[Job]
    val deferredUser = async(Job(parent = parentJob)) {
        // Simulating an exception
        throw Exception("Something went wrong!")
    }

    try {
        deferredUser.await()
    } catch (ex: Exception) {
        println("Caught exception: $ex")
    }
}
Note that the docs recommend using
SupervisorJob
rather than manually specifying Jobs for the children because this way if the parent is cancelled, the children are also cancelled
b
Thanks for the explanation