{"record":{"id":"8bd107434b5ab204","repo":"microg/GmsCore","slug":"task-this-was-cancelled-normally","errorCode":null,"errorMessage":"Task $this was cancelled normally.","messagePattern":"Task \\$this was cancelled normally\\.","errorType":"exception","errorClass":"CancellationException","httpStatus":null,"severity":"warning","filePath":"play-services-tasks/ktx/src/main/kotlin/com/google/android/gms/tasks/Tasks.kt","lineNumber":124,"sourceCode":" *\n * This suspending function is cancellable and cancellation is bi-directional:\n * * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function\n * cancels the [cancellationTokenSource] and throws a [CancellationException].\n * * If the task is cancelled, then this function will throw a [CancellationException].\n *\n * Providing a [CancellationTokenSource] that is unrelated to the receiving [Task] is not supported and\n * leads to an unspecified behaviour.\n */\n@ExperimentalCoroutinesApi // Since 1.5.1, tentatively until 1.6.0\nsuspend fun <T> Task<T>.await(cancellationTokenSource: CancellationTokenSource): T = awaitImpl(cancellationTokenSource)\n\nprivate suspend fun <T> Task<T>.awaitImpl(cancellationTokenSource: CancellationTokenSource?): T {\n    // fast path\n    if (isComplete) {\n        val e = exception\n        return if (e == null) {\n            if (isCanceled) {\n                throw CancellationException(\"Task $this was cancelled normally.\")\n            } else {\n                @Suppress(\"UNCHECKED_CAST\")\n                result as T\n            }\n        } else {\n            throw e\n        }\n    }\n\n    return suspendCancellableCoroutine { cont ->\n        addOnCompleteListener {\n            val e = it.exception\n            if (e == null) {\n                @Suppress(\"UNCHECKED_CAST\")\n                if (it.isCanceled) cont.cancel() else cont.resume(it.result as T)\n            } else {\n                cont.resumeWithException(e)\n            }","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/microg/GmsCore/blob/157c9d86ac46c195a86c2f15ab55c84036223f95/play-services-tasks/ktx/src/main/kotlin/com/google/android/gms/tasks/Tasks.kt#L106-L142","documentation":"play-services-tasks Kotlin await() throws this CancellationException when the underlying GMS Task completes in a cancelled state (isCanceled == true, no exception, no result). await() suspends on the Task's completion; if the task was cancelled (e.g. via a CancellationToken or cancellation propagated by the producer), there is no result to return, so the await itself cancels cooperatively with this message.","triggerScenarios":"Calling `task.await()` (from Tasks.kt) on a Task that completes with Task.cancel() or was created from a CancellationTokenSource that was cancelled before/during execution — the fast path `if (isComplete)` sees isCanceled true.","commonSituations":"Awaiting a location or sign-in Task whose Activity/Detachables were cancelled; cancelling a coroutine scope while the Task producer invokes Task.cancel(); passing an already-cancelled CancellationToken to a Google API; race between cancellation and await().","solutions":["Check `task.isCanceled` / attach `addOnCanceledListener` before awaiting, and handle cancellation explicitly instead of expecting a result.","If the coroutine is being cancelled intentionally, wrap the await in a scope whose Job lifecycle matches the Task and treat CancellationException as normal control flow (rethrow, don't swallow).","Use `Tasks.whenComplete`/`addOnCompleteListener` style callbacks if you need to observe cancelled tasks without coroutine cancellation.","Ensure the producer isn't calling Task.cancel() due to an expired CancellationToken you created; keep the CancellationTokenSource alive as long as you intend to await."],"exampleFix":"// before\nval result = task.await()\n// after\nif (task.isComplete && task.isCanceled) {\n    // handle cancellation, e.g. return default or notify UI\n    return\n}\nval result = task.await()","handlingStrategy":"try-catch","validationCode":"if (task.isComplete && task.isCanceled) return // skip await\ntask.addOnCanceledListener { /* observe cancellation early */ }","typeGuard":"fun <T> Task<T>.hasResult(): Boolean = isComplete && !isCanceled && exception == null","tryCatchPattern":"try {\n    val result = task.await()\n} catch (e: CancellationException) {\n    throw e // cancellation is control flow: rethrow, don't swallow\n}","preventionTips":["Check isCanceled before awaiting completed tasks","Keep CancellationTokenSource alive as long as you await","Never catch and ignore CancellationException","Match coroutine Job scope to the Task lifecycle"],"tags":["coroutines","android","cancellation","tasks"],"backgroundTag":"invalid-state-transition","analyzedSha":"157c9d86ac46c195a86c2f15ab55c84036223f95","analyzedAt":"2026-09-06T17:27:33.892Z","contentChangedAt":"2026-09-06T17:27:33.892Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}