0
# Exception Handling and Cancellation
1
2
Comprehensive error handling and cancellation mechanisms for robust coroutine applications. This covers exception propagation, cancellation semantics, error recovery patterns, and custom exception handling strategies.
3
4
## Capabilities
5
6
### Core Exception Types
7
8
The fundamental exception types for coroutine error handling and cancellation.
9
10
```kotlin { .api }
11
/**
12
* Indicates that the operation was cancelled.
13
* This is a special exception that is used to cancel coroutines cooperatively.
14
*/
15
open class CancellationException(
16
message: String? = null,
17
cause: Throwable? = null
18
) : IllegalStateException(message, cause)
19
20
/**
21
* Exception thrown by withTimeout when the timeout is exceeded.
22
*/
23
class TimeoutCancellationException(
24
message: String?,
25
coroutine: Job
26
) : CancellationException(message)
27
28
/**
29
* Exception thrown when a coroutine is cancelled while suspended in a cancellable suspending function.
30
*/
31
class JobCancellationException(
32
message: String,
33
cause: Throwable? = null,
34
job: Job
35
) : CancellationException(message, cause)
36
```
37
38
### CoroutineExceptionHandler
39
40
A context element for handling uncaught exceptions in coroutines.
41
42
```kotlin { .api }
43
/**
44
* An optional element in the coroutine context to handle uncaught exceptions.
45
* Normally, uncaught exceptions can only result from root coroutines created using
46
* launch builder. A child coroutine that fails cancels its parent and thus all siblings.
47
*/
48
interface CoroutineExceptionHandler : CoroutineContext.Element {
49
companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
50
51
/**
52
* Handles uncaught exception in the given context.
53
*/
54
fun handleException(context: CoroutineContext, exception: Throwable)
55
}
56
57
/**
58
* Creates a CoroutineExceptionHandler with the specified handler function.
59
*/
60
inline fun CoroutineExceptionHandler(
61
crossinline handler: (CoroutineContext, Throwable) -> Unit
62
): CoroutineExceptionHandler
63
```
64
65
**Usage Examples:**
66
67
```kotlin
68
import kotlinx.coroutines.*
69
70
val scope = MainScope()
71
72
// Basic exception handler
73
val exceptionHandler = CoroutineExceptionHandler { context, exception ->
74
println("Caught exception in ${context[CoroutineName]}: ${exception.message}")
75
logException(exception)
76
}
77
78
scope.launch(exceptionHandler + CoroutineName("DataProcessor")) {
79
throw RuntimeException("Something went wrong!")
80
}
81
// Output: Caught exception in DataProcessor: Something went wrong!
82
83
// Exception handler for specific coroutine types
84
val networkExceptionHandler = CoroutineExceptionHandler { context, exception ->
85
when (exception) {
86
is java.net.ConnectException -> handleNetworkError(exception)
87
is java.net.SocketTimeoutException -> handleTimeoutError(exception)
88
else -> handleGenericError(exception)
89
}
90
}
91
92
scope.launch(networkExceptionHandler) {
93
val data = fetchFromNetwork() // May throw network exceptions
94
processData(data)
95
}
96
97
// Global exception handler
98
val globalHandler = CoroutineExceptionHandler { context, exception ->
99
// Log to crash reporting service
100
crashReporter.logException(exception, context)
101
102
// Show user-friendly error message
103
when (exception) {
104
is CancellationException -> {
105
// Don't report cancellation as an error
106
println("Operation was cancelled")
107
}
108
else -> {
109
showErrorDialog("An unexpected error occurred: ${exception.message}")
110
}
111
}
112
}
113
114
// Use with scope
115
val applicationScope = CoroutineScope(
116
SupervisorJob() + Dispatchers.Main + globalHandler
117
)
118
```
119
120
### Exception Propagation Patterns
121
122
Understanding how exceptions propagate through coroutine hierarchies.
123
124
```kotlin
125
import kotlinx.coroutines.*
126
127
val scope = MainScope()
128
129
// Exception propagation in regular scope
130
scope.launch {
131
println("Parent started")
132
133
try {
134
coroutineScope {
135
launch {
136
delay(100)
137
println("Child 1 completed")
138
}
139
140
launch {
141
delay(200)
142
throw RuntimeException("Child 2 failed")
143
}
144
145
launch {
146
delay(300)
147
println("Child 3 completed") // Won't execute
148
}
149
}
150
} catch (e: Exception) {
151
println("Caught exception: ${e.message}")
152
}
153
154
println("Parent completed")
155
}
156
157
// Exception isolation with supervisor scope
158
scope.launch {
159
println("Supervisor parent started")
160
161
supervisorScope {
162
launch {
163
delay(100)
164
println("Supervisor child 1 completed")
165
}
166
167
launch {
168
try {
169
delay(200)
170
throw RuntimeException("Supervisor child 2 failed")
171
} catch (e: Exception) {
172
println("Child caught its own exception: ${e.message}")
173
}
174
}
175
176
launch {
177
delay(300)
178
println("Supervisor child 3 completed") // Will execute
179
}
180
}
181
182
println("Supervisor parent completed")
183
}
184
185
// Exception handling with async
186
scope.launch {
187
supervisorScope {
188
val deferred1 = async {
189
delay(100)
190
"Success 1"
191
}
192
193
val deferred2 = async {
194
delay(200)
195
throw RuntimeException("Async task failed")
196
}
197
198
val deferred3 = async {
199
delay(300)
200
"Success 3"
201
}
202
203
// Handle each async result individually
204
val result1 = try { deferred1.await() } catch (e: Exception) { "Failed: ${e.message}" }
205
val result2 = try { deferred2.await() } catch (e: Exception) { "Failed: ${e.message}" }
206
val result3 = try { deferred3.await() } catch (e: Exception) { "Failed: ${e.message}" }
207
208
println("Results: [$result1, $result2, $result3]")
209
}
210
}
211
```
212
213
### Cancellation Handling
214
215
Proper handling of cancellation in coroutines and cleanup operations.
216
217
```kotlin
218
import kotlinx.coroutines.*
219
220
val scope = MainScope()
221
222
// Basic cancellation handling
223
val job = scope.launch {
224
try {
225
repeat(100) { i ->
226
println("Working on item $i")
227
delay(100)
228
}
229
} catch (e: CancellationException) {
230
println("Work was cancelled")
231
throw e // Always re-throw CancellationException
232
} finally {
233
println("Cleanup completed")
234
}
235
}
236
237
// Cancel after 3 seconds
238
scope.launch {
239
delay(3000)
240
job.cancel("User requested cancellation")
241
}
242
243
// Non-cancellable cleanup
244
scope.launch {
245
try {
246
println("Starting work...")
247
performWork()
248
} catch (e: CancellationException) {
249
println("Work cancelled, performing cleanup...")
250
251
// Critical cleanup that must complete
252
withContext(NonCancellable) {
253
saveStateToFile()
254
closeResources()
255
notifyServer()
256
}
257
258
throw e
259
}
260
}
261
262
// Cancellation with timeout
263
suspend fun operationWithTimeout(): String {
264
return try {
265
withTimeout(5000) {
266
longRunningOperation()
267
}
268
} catch (e: TimeoutCancellationException) {
269
println("Operation timed out, performing cleanup...")
270
cleanupAfterTimeout()
271
throw e
272
}
273
}
274
275
// Cooperative cancellation checking
276
suspend fun longRunningTask() {
277
repeat(10000) { i ->
278
// Check for cancellation every 100 iterations
279
if (i % 100 == 0) {
280
ensureActive() // Throws CancellationException if cancelled
281
}
282
283
performWorkItem(i)
284
}
285
}
286
287
// Cancellation with resource management
288
class ResourceManager : Closeable {
289
private val resources = mutableListOf<Resource>()
290
291
suspend fun doWork() {
292
try {
293
val resource1 = acquireResource("Resource1")
294
resources.add(resource1)
295
296
val resource2 = acquireResource("Resource2")
297
resources.add(resource2)
298
299
// Perform work that may be cancelled
300
performWorkWithResources(resource1, resource2)
301
302
} catch (e: CancellationException) {
303
println("Work cancelled, cleaning up resources...")
304
throw e
305
} finally {
306
close() // Always cleanup resources
307
}
308
}
309
310
override fun close() {
311
resources.reversed().forEach { resource ->
312
try {
313
resource.release()
314
} catch (e: Exception) {
315
println("Error releasing resource: ${e.message}")
316
}
317
}
318
resources.clear()
319
}
320
}
321
```
322
323
### Error Recovery Patterns
324
325
Strategies for recovering from failures and implementing resilient systems.
326
327
```kotlin
328
import kotlinx.coroutines.*
329
import kotlin.time.Duration.Companion.seconds
330
331
// Retry with exponential backoff
332
suspend fun <T> retryWithBackoff(
333
maxRetries: Int = 3,
334
initialDelay: Long = 1000,
335
factor: Double = 2.0,
336
operation: suspend () -> T
337
): T {
338
var delay = initialDelay
339
var lastException: Exception? = null
340
341
repeat(maxRetries) { attempt ->
342
try {
343
return operation()
344
} catch (e: CancellationException) {
345
throw e // Don't retry cancellation
346
} catch (e: Exception) {
347
lastException = e
348
println("Attempt ${attempt + 1} failed: ${e.message}")
349
350
if (attempt < maxRetries - 1) {
351
println("Retrying in ${delay}ms...")
352
delay(delay)
353
delay = (delay * factor).toLong()
354
}
355
}
356
}
357
358
throw lastException ?: RuntimeException("All retry attempts failed")
359
}
360
361
// Usage
362
scope.launch {
363
try {
364
val result = retryWithBackoff(maxRetries = 3) {
365
unreliableNetworkCall()
366
}
367
println("Success: $result")
368
} catch (e: Exception) {
369
println("All retries failed: ${e.message}")
370
}
371
}
372
373
// Circuit breaker pattern
374
class CircuitBreaker(
375
private val failureThreshold: Int = 5,
376
private val resetTimeoutMs: Long = 60000
377
) {
378
private enum class State { CLOSED, OPEN, HALF_OPEN }
379
380
private var state = State.CLOSED
381
private var failureCount = 0
382
private var lastFailureTime = 0L
383
384
suspend fun <T> execute(operation: suspend () -> T): T {
385
when (state) {
386
State.OPEN -> {
387
if (System.currentTimeMillis() - lastFailureTime >= resetTimeoutMs) {
388
state = State.HALF_OPEN
389
} else {
390
throw RuntimeException("Circuit breaker is OPEN")
391
}
392
}
393
State.HALF_OPEN -> {
394
// Allow one request to test if service is back
395
}
396
State.CLOSED -> {
397
// Normal operation
398
}
399
}
400
401
return try {
402
val result = operation()
403
onSuccess()
404
result
405
} catch (e: Exception) {
406
onFailure()
407
throw e
408
}
409
}
410
411
private fun onSuccess() {
412
failureCount = 0
413
state = State.CLOSED
414
}
415
416
private fun onFailure() {
417
failureCount++
418
lastFailureTime = System.currentTimeMillis()
419
420
if (failureCount >= failureThreshold) {
421
state = State.OPEN
422
}
423
}
424
}
425
426
val circuitBreaker = CircuitBreaker()
427
428
scope.launch {
429
try {
430
val result = circuitBreaker.execute {
431
unreliableService()
432
}
433
println("Service call succeeded: $result")
434
} catch (e: Exception) {
435
println("Service call failed: ${e.message}")
436
}
437
}
438
439
// Fallback pattern
440
suspend fun fetchDataWithFallback(id: String): Data {
441
return try {
442
primaryDataSource.fetch(id)
443
} catch (e: Exception) {
444
println("Primary source failed: ${e.message}")
445
446
try {
447
secondaryDataSource.fetch(id)
448
} catch (e2: Exception) {
449
println("Secondary source failed: ${e2.message}")
450
451
// Final fallback to cache
452
cacheDataSource.fetch(id) ?: Data.empty(id)
453
}
454
}
455
}
456
457
// Bulkhead pattern - isolate failures
458
class ServiceManager {
459
private val userServiceScope = CoroutineScope(
460
SupervisorJob() + Dispatchers.Default + CoroutineName("UserService")
461
)
462
private val orderServiceScope = CoroutineScope(
463
SupervisorJob() + Dispatchers.Default + CoroutineName("OrderService")
464
)
465
private val notificationServiceScope = CoroutineScope(
466
SupervisorJob() + Dispatchers.Default + CoroutineName("NotificationService")
467
)
468
469
suspend fun processUser(userId: String): UserResult {
470
return try {
471
userServiceScope.async {
472
userService.process(userId)
473
}.await()
474
} catch (e: Exception) {
475
UserResult.Failed(e.message ?: "User processing failed")
476
}
477
}
478
479
suspend fun processOrder(orderId: String): OrderResult {
480
return try {
481
orderServiceScope.async {
482
orderService.process(orderId)
483
}.await()
484
} catch (e: Exception) {
485
OrderResult.Failed(e.message ?: "Order processing failed")
486
}
487
}
488
489
fun shutdown() {
490
userServiceScope.cancel()
491
orderServiceScope.cancel()
492
notificationServiceScope.cancel()
493
}
494
}
495
```
496
497
### Advanced Exception Handling
498
499
Sophisticated error handling patterns for complex applications.
500
501
```kotlin
502
// Result-based error handling
503
sealed class Result<out T> {
504
data class Success<T>(val value: T) : Result<T>()
505
data class Failure(val exception: Throwable) : Result<Nothing>()
506
507
inline fun <R> map(transform: (T) -> R): Result<R> = when (this) {
508
is Success -> Success(transform(value))
509
is Failure -> this
510
}
511
512
inline fun <R> flatMap(transform: (T) -> Result<R>): Result<R> = when (this) {
513
is Success -> transform(value)
514
is Failure -> this
515
}
516
517
inline fun onSuccess(action: (T) -> Unit): Result<T> = apply {
518
if (this is Success) action(value)
519
}
520
521
inline fun onFailure(action: (Throwable) -> Unit): Result<T> = apply {
522
if (this is Failure) action(exception)
523
}
524
}
525
526
suspend fun <T> safeCall(operation: suspend () -> T): Result<T> {
527
return try {
528
Result.Success(operation())
529
} catch (e: CancellationException) {
530
throw e // Don't wrap cancellation
531
} catch (e: Exception) {
532
Result.Failure(e)
533
}
534
}
535
536
// Usage
537
scope.launch {
538
val result = safeCall { riskyOperation() }
539
.map { processResult(it) }
540
.onSuccess { println("Operation succeeded: $it") }
541
.onFailure { println("Operation failed: ${it.message}") }
542
}
543
544
// Error aggregation
545
suspend fun processMultipleItems(items: List<String>): ProcessingReport {
546
val results = mutableListOf<ItemResult>()
547
val errors = mutableListOf<ProcessingError>()
548
549
supervisorScope {
550
items.map { item ->
551
async {
552
try {
553
val result = processItem(item)
554
results.add(ItemResult.Success(item, result))
555
} catch (e: CancellationException) {
556
throw e
557
} catch (e: Exception) {
558
val error = ProcessingError(item, e.message ?: "Unknown error")
559
errors.add(error)
560
results.add(ItemResult.Failed(item, error))
561
}
562
}
563
}.awaitAll()
564
}
565
566
return ProcessingReport(
567
totalItems = items.size,
568
successCount = results.count { it is ItemResult.Success },
569
failureCount = errors.size,
570
errors = errors
571
)
572
}
573
574
// Custom exception hierarchy
575
sealed class BusinessException(message: String, cause: Throwable? = null) : Exception(message, cause) {
576
class ValidationException(message: String) : BusinessException(message)
577
class AuthenticationException(message: String) : BusinessException(message)
578
class AuthorizationException(message: String) : BusinessException(message)
579
class ResourceNotFoundException(resource: String) : BusinessException("Resource not found: $resource")
580
class ExternalServiceException(service: String, cause: Throwable) : BusinessException("External service error: $service", cause)
581
}
582
583
// Business logic exception handler
584
val businessExceptionHandler = CoroutineExceptionHandler { context, exception ->
585
when (exception) {
586
is BusinessException.ValidationException -> {
587
showValidationError(exception.message ?: "Validation failed")
588
}
589
is BusinessException.AuthenticationException -> {
590
redirectToLogin()
591
}
592
is BusinessException.AuthorizationException -> {
593
showAccessDeniedMessage()
594
}
595
is BusinessException.ResourceNotFoundException -> {
596
showNotFoundMessage(exception.message ?: "Resource not found")
597
}
598
is BusinessException.ExternalServiceException -> {
599
showServiceUnavailableMessage()
600
logExternalServiceError(exception)
601
}
602
else -> {
603
showGenericErrorMessage()
604
logUnexpectedError(exception)
605
}
606
}
607
}
608
```
609
610
### Testing Exception Handling
611
612
Strategies for testing error scenarios and exception handling logic.
613
614
```kotlin
615
import kotlinx.coroutines.test.*
616
import kotlin.test.*
617
618
@Test
619
fun testExceptionPropagation() = runTest {
620
var exceptionHandled = false
621
622
val handler = CoroutineExceptionHandler { _, exception ->
623
exceptionHandled = true
624
assertEquals("Test exception", exception.message)
625
}
626
627
val job = launch(handler) {
628
throw RuntimeException("Test exception")
629
}
630
631
job.join()
632
assertTrue(exceptionHandled)
633
}
634
635
@Test
636
fun testCancellationHandling() = runTest {
637
var cleanupCalled = false
638
639
val job = launch {
640
try {
641
delay(1000)
642
fail("Should have been cancelled")
643
} catch (e: CancellationException) {
644
// Expected
645
throw e
646
} finally {
647
cleanupCalled = true
648
}
649
}
650
651
delay(100)
652
job.cancel()
653
job.join()
654
655
assertTrue(cleanupCalled)
656
assertTrue(job.isCancelled)
657
}
658
659
@Test
660
fun testRetryLogic() = runTest {
661
var attemptCount = 0
662
663
val result = retryWithBackoff(maxRetries = 3, initialDelay = 10) {
664
attemptCount++
665
if (attemptCount < 3) {
666
throw RuntimeException("Attempt $attemptCount failed")
667
}
668
"Success on attempt $attemptCount"
669
}
670
671
assertEquals("Success on attempt 3", result)
672
assertEquals(3, attemptCount)
673
}
674
675
@Test
676
fun testCircuitBreakerOpen() = runTest {
677
val circuitBreaker = CircuitBreaker(failureThreshold = 2)
678
679
// Cause failures to open circuit
680
repeat(2) {
681
assertFailsWith<RuntimeException> {
682
circuitBreaker.execute { throw RuntimeException("Service down") }
683
}
684
}
685
686
// Circuit should be open
687
assertFailsWith<RuntimeException> {
688
circuitBreaker.execute { "Should not execute" }
689
}
690
}
691
692
@Test
693
fun testSupervisorScopeIsolation() = runTest {
694
val results = mutableListOf<String>()
695
696
supervisorScope {
697
launch {
698
results.add("Task 1 completed")
699
}
700
701
launch {
702
throw RuntimeException("Task 2 failed")
703
}
704
705
launch {
706
delay(100) // Ensure task 2 fails first
707
results.add("Task 3 completed")
708
}
709
}
710
711
assertEquals(listOf("Task 1 completed", "Task 3 completed"), results)
712
}
713
```
714
715
### Best Practices for Exception Handling
716
717
Guidelines for robust error handling in coroutine applications.
718
719
```kotlin
720
// DO: Always re-throw CancellationException
721
suspend fun goodCancellationHandling() {
722
try {
723
performWork()
724
} catch (e: CancellationException) {
725
performCleanup()
726
throw e // CRITICAL: Always re-throw
727
} catch (e: Exception) {
728
handleBusinessException(e)
729
}
730
}
731
732
// DON'T: Swallow CancellationException
733
suspend fun badCancellationHandling() {
734
try {
735
performWork()
736
} catch (e: Exception) { // This catches CancellationException too!
737
handleException(e)
738
// Missing re-throw of CancellationException
739
}
740
}
741
742
// DO: Use appropriate scope for error isolation
743
suspend fun goodErrorIsolation() {
744
supervisorScope { // Child failures don't cancel siblings
745
val task1 = async { riskyOperation1() }
746
val task2 = async { riskyOperation2() }
747
748
val result1 = try { task1.await() } catch (e: Exception) { "Default1" }
749
val result2 = try { task2.await() } catch (e: Exception) { "Default2" }
750
751
processResults(result1, result2)
752
}
753
}
754
755
// DO: Handle exceptions at the right level
756
class DataService {
757
// Low-level: Convert specific exceptions
758
private suspend fun fetchFromNetwork(): NetworkData {
759
try {
760
return networkClient.fetch()
761
} catch (e: IOException) {
762
throw BusinessException.ExternalServiceException("Network", e)
763
}
764
}
765
766
// Mid-level: Aggregate and provide fallbacks
767
suspend fun getData(): Result<Data> {
768
return try {
769
val networkData = fetchFromNetwork()
770
Result.Success(processData(networkData))
771
} catch (e: BusinessException.ExternalServiceException) {
772
// Try cache fallback
773
getCachedData()
774
}
775
}
776
777
// High-level: Final error handling with user communication
778
suspend fun displayData() {
779
when (val result = getData()) {
780
is Result.Success -> showData(result.value)
781
is Result.Failure -> showErrorMessage(result.exception)
782
}
783
}
784
}
785
786
// DO: Use structured error handling
787
suspend fun structuredErrorHandling() {
788
val errorBoundary = CoroutineExceptionHandler { _, exception ->
789
when (exception) {
790
is CancellationException -> {
791
// Don't log cancellation as error
792
}
793
else -> {
794
logError(exception)
795
notifyMonitoring(exception)
796
}
797
}
798
}
799
800
withContext(errorBoundary) {
801
coroutineScope {
802
// All child coroutines inherit error handling
803
launch { task1() }
804
launch { task2() }
805
launch { task3() }
806
}
807
}
808
}
809
```