Why does leaving the screen log a network error and leave the temp file?
A download runs in viewModelScope. Every time the user leaves the screen mid-download the app logs "download failed: network error", and the partial temp file is never deleted.
Constraints: leaving the screen must be silent and must stop the download, the partial file must always be deleted, and a genuine network failure must still be reported. deleteTemp() suspends.
fun download(url: String) = viewModelScope.launch(Dispatchers.IO) {
try {
httpClient.writeTo(url, tempFile) // suspending
} catch (e: Exception) {
Log.e("dl", "download failed: network error", e)
} finally {
deleteTemp(tempFile) // suspending
}
}
Diagnose the cause and fix it.
Cancellation arrives as CancellationException, which catch (e: Exception) swallows — so a normal cancel is logged as a network failure. The suspending deleteTemp() in finally then fails at once, as the Job is cancelled. Rethrow it; clean up in NonCancellable.
- ✗Catching
Exception(or usingrunCatching) around a suspending call and swallowing the cancel - ✗Reporting a
CancellationExceptionto the user as if it were a real failure - ✗Expecting a suspending cleanup in
finallyto run inside a cancelled coroutine
- →Why is
runCatchingeven more dangerous here thancatch (e: Exception)? - →Which failures should this download's
catchstill handle itself?
The cause
Two bugs, both rooted in the fact that cancellation is an exception.
it too. Cancellation stops propagating and a network failure that never happened is logged. runCatching is twice as dangerous: it catches Throwable.
immediately — including deleteTemp() in the finally. The file stays on disk.
CancellationExceptionis an ordinary subclass ofException, socatch (e: Exception)catches- In a cancelled coroutine the
Jobis no longer active, so any suspending call fails
The fix
fun download(url: String) = viewModelScope.launch(Dispatchers.IO) {
try {
httpClient.writeTo(url, tempFile)
} catch (e: CancellationException) {
throw e // let the cancellation through
} catch (e: IOException) {
Log.e("dl", "download failed: network error", e)
} finally {
withContext(NonCancellable) { // cleanup survives the cancellation
deleteTemp(tempFile)
}
}
}
The CancellationException is rethrown, so leaving the screen is silent again and the coroutine really does stop. A genuine network failure is caught by the narrow catch (e: IOException). The cleanup inside NonCancellable runs to completion — keep that section short, since nothing can stop it.