How do I cancel a coroutine in Kotlin safely?

In Kotlin, you cancel a coroutine safely by calling cancel() on its Job or scope, and making sure the coroutine code is cooperative: it should suspend regularly or check for cancellation.

Basic example

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1_000) { i ->
                println("Working $i")
                delay(500) // cancellable suspension point
            }
        } finally {
            println("Cleaning up")
        }
    }

    delay(1_500)
    job.cancel()
    job.join()

    println("Cancelled safely")
}

Or more commonly:

job.cancelAndJoin()
job.cancelAndJoin()

This cancels the coroutine and waits until it finishes cleanup.

Use cancellable suspending functions

Most kotlinx.coroutines suspending functions are cancellable, for example:

delay(...)
withContext(...)
receive(...)
send(...)

So this is usually safe:

val job = scope.launch {
    while (true) {
        delay(1000)
        doWork()
    }
}

job.cancel()

For CPU-heavy loops, check cancellation manually

If your coroutine does not suspend often, cancellation will not be noticed immediately.

Use isActive:

val job = scope.launch {
    while (isActive) {
        doCpuWorkChunk()
    }
}

Or call ensureActive():

val job = scope.launch {
    while (true) {
        ensureActive()
        doCpuWorkChunk()
    }
}

Cleanup with finally

Use try/finally for cleanup:

val job = scope.launch {
    try {
        doWork()
    } finally {
        closeResources()
    }
}

If cleanup needs to call suspending functions, use NonCancellable:

val job = scope.launch {
    try {
        doWork()
    } finally {
        withContext(NonCancellable) {
            saveState()
            closeRemoteConnection()
        }
    }
}

Do not swallow CancellationException

Cancellation is represented by CancellationException. Avoid catching it accidentally and ignoring it.

Bad:

try {
    doWork()
} catch (e: Exception) {
    // This also catches CancellationException
    logError(e)
}

Better:

try {
    doWork()
} catch (e: CancellationException) {
    throw e
} catch (e: Exception) {
    logError(e)
}

Cancel a scope

If you created a scope, cancel it when the owner is destroyed:

class Repository {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    fun start() {
        scope.launch {
            doWork()
        }
    }

    fun close() {
        scope.cancel()
    }
}

Summary

Use:

job.cancelAndJoin()

Make your coroutine cooperative by using:

delay(...)
isActive
ensureActive()

Clean up with:

try {
    // work
} finally {
    // cleanup
}

And avoid swallowing CancellationException.