Skip to content

Instantly share code, notes, and snippets.

@vitalikas
Last active December 11, 2025 12:49
Show Gist options
  • Select an option

  • Save vitalikas/b640a416e149df9a5aa35941b5cc31ef to your computer and use it in GitHub Desktop.

Select an option

Save vitalikas/b640a416e149df9a5aa35941b5cc31ef to your computer and use it in GitHub Desktop.
Coroutine cancellation examples
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
inline fun <R> runCatchingWithCancellation(block: () -> R): Result<R> {
return try {
Result.success(block())
} catch (e: CancellationException) {
throw e // Re-throw immediately - do NOT wrap in Result.failure()
} catch (e: Throwable) {
Result.failure(e)
}
}
fun main() = runBlocking {
println("========== EXAMPLE 1: CancellationException Does NOT Cancel Parent ==========")
val parentJob = launch {
println("Parent: Starting child coroutines...")
val child1 = launch {
try {
println(" Child1: Working...")
delay(2000)
println(" Child1: Completed")
} catch (e: CancellationException) {
println(" Child1: Cancelled!")
}
}
val child2 = launch {
try {
println(" Child2: Working...")
delay(500)
println(" Child2: Throwing IllegalStateException!")
throw IllegalStateException()
} catch (e: IllegalStateException) {
println(" Child2: Caught ${e.javaClass.simpleName}")
}
}
child1.join()
child2.join()
println("Parent: Completed")
}
parentJob.join()
println("Result: Parent job is cancelled = ${parentJob.isCancelled}\n")
println("========== EXAMPLE 2: Regular Exception DOES Cancel Parent ==========")
// Use SupervisorJob to prevent exception from cancelling runBlocking
val exceptionHandler2 = CoroutineExceptionHandler { _, exception ->
println("Caught exception in handler: ${exception.javaClass.simpleName}: ${exception.message}")
}
val parentJob2 = launch(SupervisorJob() + exceptionHandler2) {
println("Parent2: Starting child coroutines...")
val child1 = launch {
try {
println(" Child1: Working...")
delay(2000)
println(" Child1: Completed")
} catch (e: CancellationException) {
// CancellationException from parent - no need to re-throw
println(" Child1: Cancelled!")
}
}
val child2 = launch {
println(" Child2: Working...")
delay(500)
println(" Child2: Throwing IllegalStateException!")
throw IllegalStateException("Child2 failed - this WILL cancel parent!")
}
child1.join()
child2.join()
println("Parent2: Completed")
}
parentJob2.join()
println("Result: Parent2 job is cancelled = ${parentJob2.isCancelled}\n")
println("========== EXAMPLE 3: Standard runCatching - Swallows Cancellation ==========")
val parentJob3 = launch {
println("Parent3: Starting...")
launch {
// Standard runCatching wraps CancellationException in Result.failure()
runCatching {
delay(500)
throw CancellationException("Trying to cancel")
}
println(" ✗ CancellationException caught in Result.failure()")
println(" ✗ Cancellation swallowed - child coroutine continues!")
}
delay(1000)
println("Parent3: Completed (prints because cancellation was swallowed)")
}
parentJob3.join()
println("Result: Parent3 job is cancelled = ${parentJob3.isCancelled}\n")
println("========== EXAMPLE 4: Our runCatchingWithCancellation - Preserves Cancellation ==========")
val parentJob4 = launch {
println("Parent4: Starting...")
launch {
// Our version re-throws CancellationException
runCatchingWithCancellation {
delay(500)
throw CancellationException()
}
println(" This line won't execute")
}
delay(1000)
println("Parent4: Completed (prints - CancellationException only cancelled the child)")
}
parentJob4.join()
println("Result: Parent4 job is cancelled = ${parentJob4.isCancelled}\n")
println("========== EXAMPLE 5: Real-World Repository Pattern ==========")
// Simulates a repository that fetches user data
class UserRepository {
suspend fun getUserWithStandardRunCatching(userId: Int): Result<String> {
return runCatching {
println(" [Wrong] Fetching user $userId...")
delay(2000) // Simulates network call
"User_$userId"
}
}
suspend fun getUserWithRunCatchingWithCancellation(userId: Int): Result<String> {
return runCatchingWithCancellation {
println(" [Correct] Fetching user $userId...")
delay(2000) // Simulates network call
"User_$userId"
}
}
}
val repository = UserRepository()
// Scenario: User navigates away quickly
println("\nScenario: User opens screen, then immediately navigates back\n")
// WRONG: Standard runCatching
println("❌ Using standard runCatching:")
val wrongJob = launch {
val result = repository.getUserWithStandardRunCatching(123)
// result lambda (onSuccess { } or onFailure { }) is executed even after cancellation
result
.onSuccess {
println(" [Wrong] success block: $it")
}
.onFailure {
println(" [Wrong] error block: $it")
}
}
delay(500) // User navigates away after 500ms
println(" User navigated away - cancelling job...")
wrongJob.cancel()
wrongJob.join() // Wait for job to finish (only needed in demo to see the behavior)
delay(2000) // Wait to show the network call completes anyway
println("\n✅ Using runCatchingWithCancellation:")
val correctJob = launch {
val result = repository.getUserWithRunCatchingWithCancellation(456)
// result lambda (onSuccess { } or onFailure { }) is NEVER executed because job was cancelled
result // ← Execution NEVER reaches here
.onSuccess {
println(" success block: $it")
}
.onFailure {
println(" error block: $it")
}
}
delay(500) // User navigates away after 500ms
println(" User navigated away - cancelling job...")
correctJob.cancel()
correctJob.join() // Wait for job to finish (only needed in demo to see the behavior)
delay(2000) // Network call stops immediately, nothing happens
println("\nResult: correctJob was properly cancelled = ${correctJob.isCancelled}")
println("========== KEY TAKEAWAY ==========")
println("\n1. CancellationException behavior:")
println(" • Cancels ONLY the coroutine that throws it (not parent/siblings)")
println(" • Regular exceptions cancel parent, siblings, and propagate up")
println("\n2. The runCatching problem:")
println(" • kotlin.runCatching wraps CancellationException → breaks cancellation")
println(" • When job.cancel() is called, delay() throws CancellationException")
println(" • Standard runCatching catches it → operation continues!")
println("\n3. The solution - runCatchingWithCancellation:")
println(" • Re-throws CancellationException → preserves cancellation")
println(" • Still catches business errors (IOException, etc.) in Result.failure()")
println(" • Use in repositories: return runCatchingWithCancellation { api.call() }")
println("\n4. Real-world cancellation sources (automatic, not manual):")
println(" • viewModelScope.cancel() when screen is destroyed")
println(" • lifecycleScope.cancel() when lifecycle ends")
println(" • job.cancel() when user navigates back")
println(" • withTimeout() when timeout expires")
println(" • delay() checks cancellation and throws CancellationException")
println("\n5. Why it matters:")
println(" • Saves battery (network calls stop)")
println(" • Saves bandwidth (no unnecessary data transfer)")
println(" • Saves memory (no processing unused results)")
println(" • Better UX (app responds faster to navigation)")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment