microg/GmsCore · warning · CancellationException

Task $this was cancelled normally.

Error message

Task $this was cancelled normally.

What it means

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.

Source

Thrown at play-services-tasks/ktx/src/main/kotlin/com/google/android/gms/tasks/Tasks.kt:124

 *
 * This suspending function is cancellable and cancellation is bi-directional:
 * * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
 * cancels the [cancellationTokenSource] and throws a [CancellationException].
 * * If the task is cancelled, then this function will throw a [CancellationException].
 *
 * Providing a [CancellationTokenSource] that is unrelated to the receiving [Task] is not supported and
 * leads to an unspecified behaviour.
 */
@ExperimentalCoroutinesApi // Since 1.5.1, tentatively until 1.6.0
suspend fun <T> Task<T>.await(cancellationTokenSource: CancellationTokenSource): T = awaitImpl(cancellationTokenSource)

private suspend fun <T> Task<T>.awaitImpl(cancellationTokenSource: CancellationTokenSource?): T {
    // fast path
    if (isComplete) {
        val e = exception
        return if (e == null) {
            if (isCanceled) {
                throw CancellationException("Task $this was cancelled normally.")
            } else {
                @Suppress("UNCHECKED_CAST")
                result as T
            }
        } else {
            throw e
        }
    }

    return suspendCancellableCoroutine { cont ->
        addOnCompleteListener {
            val e = it.exception
            if (e == null) {
                @Suppress("UNCHECKED_CAST")
                if (it.isCanceled) cont.cancel() else cont.resume(it.result as T)
            } else {
                cont.resumeWithException(e)
            }

View on GitHub (pinned to 157c9d86ac)

Solutions

  1. Check `task.isCanceled` / attach `addOnCanceledListener` before awaiting, and handle cancellation explicitly instead of expecting a result.
  2. 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).
  3. Use `Tasks.whenComplete`/`addOnCompleteListener` style callbacks if you need to observe cancelled tasks without coroutine cancellation.
  4. 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.

Example fix

// before
val result = task.await()
// after
if (task.isComplete && task.isCanceled) {
    // handle cancellation, e.g. return default or notify UI
    return
}
val result = task.await()
Defensive patterns

Strategy: try-catch

Validate before calling

if (task.isComplete && task.isCanceled) return // skip await
task.addOnCanceledListener { /* observe cancellation early */ }

Type guard

fun <T> Task<T>.hasResult(): Boolean = isComplete && !isCanceled && exception == null

Try / catch

try {
    val result = task.await()
} catch (e: CancellationException) {
    throw e // cancellation is control flow: rethrow, don't swallow
}

Prevention

When it happens

Trigger: 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.

Common situations: 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().

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of microg/GmsCore@157c9d86ac (2026-09-06). Data as JSON: /api/errors/8bd107434b5ab204. Report an issue: GitHub.