Last active
December 11, 2025 12:49
-
-
Save vitalikas/b640a416e149df9a5aa35941b5cc31ef to your computer and use it in GitHub Desktop.
Coroutine cancellation examples
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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