LukasLechnerDev / Kotlin-Coroutines-and-Flow-UseCases-on-Android

🎓 Learning Kotlin Coroutines and Flows for Android by example. 🚀 Sample implementations for real-world Android use cases. 🛠 Unit tests included!
Apache License 2.0
2.63k stars 433 forks source link

usecase4/VariableAmountOfNetworkRequestsViewModel#performNetworkRequestsConcurrently crashes on exception #8

Open lgtout opened 3 years ago

lgtout commented 3 years ago

Hi, Lukas!

I think there's a problem with the solution for

usecase4/VariableAmountOfNetworkRequestsViewModel#performNetworkRequestsConcurrently.

It crashes when I configure MockApi to return a 500 on the call to http://localhost/android-version-features/28.

Reading this about exceptions, it seems like try-catch will not catch the exception because async is not the direct child of the scope - launch is. So async will propagate the exception up to its parent (launch) and launch will throw the exception.

What do you think?

- Julian

lgtout commented 3 years ago

Just did some more investigating. I see that the exception gets caught by the try-catch. It is kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=StandaloneCoroutine{Cancelling}@926b1ac Then the app crashes.

lgtout commented 3 years ago

I read your post about coroutine exceptions, which confirmed that the try-catch inside launch will catch calls to getAndroidVersions(), but will not catch calls to getAndroidVersionFeatures() because those take place within a non-top-level async.

Any exception thrown within the async is propagated to the root launch. And the only way to catch an exception propagated in this way is by installing a CoroutineExceptionHandler.

The following covers all exception cases:

fun performNetworkRequestsConcurrently() {
    uiState.value = UiState.Loading
    viewModelScope.launch(CoroutineExceptionHandler {
        _, throwable ->
        Timber.v(throwable)
        uiState.value = UiState.Error("Network Request failed")
    }) {
        try {
            val recentVersions = mockApi.getRecentAndroidVersions()
            val versionFeaturesJobs = recentVersions.map {
                androidVersion ->
                async {
                    mockApi.getAndroidVersionFeatures(
                            androidVersion.apiLevel
                    )
                }
            }
            val versionFeatures = versionFeaturesJobs.awaitAll()
            uiState.value = UiState.Success(versionFeatures)
        } catch (exception: Exception) {
            uiState.value = UiState.Error("Network Request failed")
        }
    }
}
kaal-dam commented 9 months ago

Hello @LukasLechnerDev it seems that the issue described here is still present in the current version of the course

Using async directly rather than viewModelScope.async result in crash when the api response is in error