0
# Error Handling & Cancellation
1
2
Cancellation propagation, exception handling, and timeout management for robust coroutine applications. kotlinx-coroutines provides structured error handling that automatically propagates cancellation and manages exceptions.
3
4
## Capabilities
5
6
### Cancellation
7
8
Cooperative cancellation mechanism for coroutines.
9
10
```kotlin { .api }
11
/**
12
* Base exception for coroutine cancellation
13
*/
14
open class CancellationException : IllegalStateException {
15
constructor()
16
constructor(message: String?)
17
constructor(message: String?, cause: Throwable?)
18
}
19
20
/**
21
* Check if current coroutine is active
22
*/
23
val CoroutineScope.isActive: Boolean
24
25
/**
26
* Ensure coroutine is active, throw CancellationException if cancelled
27
*/
28
fun CoroutineScope.ensureActive()
29
30
/**
31
* Yield to other coroutines and check for cancellation
32
*/
33
suspend fun yield()
34
```
35
36
**Usage Examples:**
37
38
```kotlin
39
import kotlinx.coroutines.*
40
41
suspend fun cancellableWork() {
42
repeat(1000) { i ->
43
// Check for cancellation periodically
44
ensureActive()
45
46
// Or use yield() which also checks cancellation
47
yield()
48
49
// Perform work
50
processItem(i)
51
}
52
}
53
54
// Cancelling coroutines
55
val job = launch {
56
try {
57
cancellableWork()
58
} catch (e: CancellationException) {
59
println("Work was cancelled")
60
// Don't suppress CancellationException
61
throw e
62
}
63
}
64
65
// Cancel after delay
66
delay(100)
67
job.cancel("Timeout reached")
68
```
69
70
### Timeout Operations
71
72
Functions for adding timeouts to coroutine operations.
73
74
```kotlin { .api }
75
/**
76
* Execute block with timeout, throw TimeoutCancellationException on timeout
77
*/
78
suspend fun <T> withTimeout(timeoutMillis: Long, block: suspend CoroutineScope.() -> T): T
79
80
/**
81
* Execute block with timeout, return null on timeout
82
*/
83
suspend fun <T> withTimeoutOrNull(timeoutMillis: Long, block: suspend CoroutineScope.() -> T): T?
84
85
/**
86
* Exception thrown by withTimeout
87
*/
88
class TimeoutCancellationException : CancellationException {
89
val coroutine: Job?
90
}
91
```
92
93
**Usage Examples:**
94
95
```kotlin
96
import kotlinx.coroutines.*
97
98
// Timeout with exception
99
try {
100
val result = withTimeout(1000) {
101
longRunningOperation()
102
}
103
println("Completed: $result")
104
} catch (e: TimeoutCancellationException) {
105
println("Operation timed out")
106
}
107
108
// Timeout with null return
109
val result = withTimeoutOrNull(1000) {
110
longRunningOperation()
111
}
112
113
if (result != null) {
114
println("Completed: $result")
115
} else {
116
println("Operation timed out")
117
}
118
119
// Network request with timeout
120
suspend fun fetchDataWithTimeout(url: String): String? {
121
return withTimeoutOrNull(5000) {
122
httpClient.get(url)
123
}
124
}
125
```
126
127
### Exception Handling
128
129
Structured exception handling in coroutines.
130
131
```kotlin { .api }
132
/**
133
* Exception handler for coroutines
134
*/
135
interface CoroutineExceptionHandler : CoroutineContext.Element {
136
/**
137
* Handle uncaught exception
138
*/
139
fun handleException(context: CoroutineContext, exception: Throwable)
140
141
companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
142
}
143
144
/**
145
* Create exception handler
146
*/
147
fun CoroutineExceptionHandler(handler: (CoroutineContext, Throwable) -> Unit): CoroutineExceptionHandler
148
```
149
150
**Usage Examples:**
151
152
```kotlin
153
import kotlinx.coroutines.*
154
155
// Global exception handler
156
val handler = CoroutineExceptionHandler { _, exception ->
157
println("Caught exception: ${exception.localizedMessage}")
158
}
159
160
// Use with launch (only works with root coroutines)
161
val scope = CoroutineScope(SupervisorJob() + handler)
162
163
scope.launch {
164
throw RuntimeException("Something went wrong")
165
}
166
167
// Exception handling in async
168
val deferred = async {
169
throw RuntimeException("Async error")
170
}
171
172
try {
173
deferred.await() // Exception thrown here
174
} catch (e: RuntimeException) {
175
println("Caught async exception: $e")
176
}
177
```
178
179
### Supervisor Jobs
180
181
Jobs that don't cancel children when one child fails.
182
183
```kotlin { .api }
184
/**
185
* Create supervisor job where child failures don't affect siblings
186
*/
187
fun SupervisorJob(parent: Job? = null): CompletableJob
188
189
/**
190
* Create supervisor scope
191
*/
192
suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R
193
```
194
195
**Usage Examples:**
196
197
```kotlin
198
import kotlinx.coroutines.*
199
200
// Supervisor job - child failures don't cancel siblings
201
val supervisorJob = SupervisorJob()
202
val scope = CoroutineScope(supervisorJob + Dispatchers.Default)
203
204
// These jobs are independent
205
scope.launch {
206
delay(100)
207
throw RuntimeException("Job 1 failed")
208
}
209
210
scope.launch {
211
delay(200)
212
println("Job 2 completed successfully") // Still runs
213
}
214
215
// Supervisor scope
216
supervisorScope {
217
launch {
218
throw RuntimeException("Child 1 failed")
219
}
220
221
launch {
222
delay(100)
223
println("Child 2 completed") // Still completes
224
}
225
226
println("Supervisor scope completed")
227
}
228
```
229
230
### Non-Cancellable Context
231
232
Context that prevents cancellation for cleanup operations.
233
234
```kotlin { .api }
235
/**
236
* Non-cancellable job for cleanup operations
237
*/
238
object NonCancellable : CoroutineContext.Element, Job {
239
override val isActive: Boolean get() = true
240
override val isCompleted: Boolean get() = false
241
override val isCancelled: Boolean get() = false
242
// ... other Job methods are no-ops
243
}
244
```
245
246
**Usage Examples:**
247
248
```kotlin
249
import kotlinx.coroutines.*
250
251
suspend fun operationWithCleanup() {
252
try {
253
// Cancellable operation
254
performWork()
255
} finally {
256
// Non-cancellable cleanup
257
withContext(NonCancellable) {
258
// This cleanup code will run even if coroutine is cancelled
259
releaseResources()
260
saveState()
261
}
262
}
263
}
264
265
// File operations with guaranteed cleanup
266
suspend fun processFile(filename: String) {
267
val file = openFile(filename)
268
try {
269
processFileContent(file)
270
} finally {
271
withContext(NonCancellable) {
272
// Always close file, even if cancelled
273
file.close()
274
}
275
}
276
}
277
```
278
279
## Exception Handling Patterns
280
281
### Try-Catch in Coroutines
282
283
Normal exception handling works in coroutines:
284
285
```kotlin
286
suspend fun handleExceptions() {
287
try {
288
val result = riskyOperation()
289
processResult(result)
290
} catch (e: NetworkException) {
291
handleNetworkError(e)
292
} catch (e: ParseException) {
293
handleParseError(e)
294
} finally {
295
cleanup()
296
}
297
}
298
```
299
300
### Async Error Handling
301
302
Exceptions in async are stored until await() is called:
303
304
```kotlin
305
suspend fun asyncErrorHandling() {
306
val deferred1 = async { mayFailOperation1() }
307
val deferred2 = async { mayFailOperation2() }
308
309
// Exceptions thrown here when awaited
310
try {
311
val result1 = deferred1.await()
312
val result2 = deferred2.await()
313
processResults(result1, result2)
314
} catch (e: Exception) {
315
handleError(e)
316
}
317
}
318
319
// Multiple async with individual error handling
320
suspend fun individualAsyncErrors() {
321
val deferred1 = async { mayFailOperation1() }
322
val deferred2 = async { mayFailOperation2() }
323
324
val result1 = try {
325
deferred1.await()
326
} catch (e: Exception) {
327
handleError1(e)
328
null
329
}
330
331
val result2 = try {
332
deferred2.await()
333
} catch (e: Exception) {
334
handleError2(e)
335
null
336
}
337
338
processResults(result1, result2)
339
}
340
```
341
342
### Launch Error Handling
343
344
Errors in launch propagate to parent unless handled:
345
346
```kotlin
347
// Unhandled exceptions crash the parent
348
launch {
349
throw RuntimeException("This will crash parent")
350
}
351
352
// Handle exceptions in launch
353
launch {
354
try {
355
riskyOperation()
356
} catch (e: Exception) {
357
handleError(e)
358
}
359
}
360
361
// Or use exception handler
362
val handler = CoroutineExceptionHandler { _, exception ->
363
handleGlobalError(exception)
364
}
365
366
launch(handler) {
367
throw RuntimeException("Handled by exception handler")
368
}
369
```
370
371
## Advanced Error Handling
372
373
### Retry Logic
374
375
Implementing retry patterns with exponential backoff:
376
377
```kotlin
378
suspend fun <T> retryWithBackoff(
379
times: Int = 3,
380
initialDelay: Long = 100,
381
maxDelay: Long = 1000,
382
factor: Double = 2.0,
383
block: suspend () -> T
384
): T {
385
var currentDelay = initialDelay
386
repeat(times - 1) {
387
try {
388
return block()
389
} catch (e: Exception) {
390
delay(currentDelay)
391
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
392
}
393
}
394
return block() // Last attempt
395
}
396
397
// Usage
398
val result = retryWithBackoff(times = 3) {
399
fetchDataFromServer()
400
}
401
```
402
403
### Circuit Breaker Pattern
404
405
```kotlin
406
class CircuitBreaker(
407
private val failureThreshold: Int = 5,
408
private val resetTimeoutMs: Long = 60000
409
) {
410
private var failures = 0
411
private var lastFailureTime = 0L
412
private var state = State.CLOSED
413
414
enum class State { CLOSED, OPEN, HALF_OPEN }
415
416
suspend fun <T> execute(block: suspend () -> T): T {
417
when (state) {
418
State.OPEN -> {
419
if (System.currentTimeMillis() - lastFailureTime > resetTimeoutMs) {
420
state = State.HALF_OPEN
421
} else {
422
throw CircuitBreakerOpenException()
423
}
424
}
425
State.HALF_OPEN -> {
426
// Allow one test call
427
}
428
State.CLOSED -> {
429
// Normal operation
430
}
431
}
432
433
return try {
434
val result = block()
435
onSuccess()
436
result
437
} catch (e: Exception) {
438
onFailure()
439
throw e
440
}
441
}
442
443
private fun onSuccess() {
444
failures = 0
445
state = State.CLOSED
446
}
447
448
private fun onFailure() {
449
failures++
450
lastFailureTime = System.currentTimeMillis()
451
if (failures >= failureThreshold) {
452
state = State.OPEN
453
}
454
}
455
}
456
```
457
458
## Best Practices
459
460
### 1. Don't Suppress CancellationException
461
462
```kotlin
463
// Wrong - suppresses cancellation
464
try {
465
work()
466
} catch (e: Exception) {
467
// This catches CancellationException too!
468
log.error("Error", e)
469
}
470
471
// Correct - let cancellation propagate
472
try {
473
work()
474
} catch (e: CancellationException) {
475
throw e // Re-throw cancellation
476
} catch (e: Exception) {
477
log.error("Error", e)
478
}
479
480
// Or be specific about what you catch
481
try {
482
work()
483
} catch (e: NetworkException) {
484
handleNetworkError(e)
485
}
486
```
487
488
### 2. Use Supervisor Scope for Independent Tasks
489
490
```kotlin
491
// Good - independent tasks
492
supervisorScope {
493
launch { backgroundTask1() }
494
launch { backgroundTask2() }
495
launch { backgroundTask3() }
496
}
497
498
// Bad - one failure cancels all
499
coroutineScope {
500
launch { backgroundTask1() }
501
launch { backgroundTask2() }
502
launch { backgroundTask3() }
503
}
504
```
505
506
### 3. Handle Timeouts Appropriately
507
508
```kotlin
509
// For operations that should timeout
510
val result = withTimeoutOrNull(5000) {
511
networkCall()
512
}
513
514
if (result == null) {
515
// Handle timeout gracefully
516
showTimeoutMessage()
517
}
518
519
// For critical operations
520
try {
521
withTimeout(30000) {
522
criticalDatabaseUpdate()
523
}
524
} catch (e: TimeoutCancellationException) {
525
// Log and potentially retry
526
logger.error("Critical operation timed out", e)
527
throw e
528
}
529
```
530
531
## Additional Exception Classes
532
533
### Channel-Related Exceptions
534
535
Exceptions specific to channel operations.
536
537
```kotlin { .api }
538
/**
539
* Exception thrown when attempting to send on a closed channel
540
*/
541
class ClosedSendChannelException(message: String? = null) : IllegalStateException(message)
542
543
/**
544
* Exception thrown when attempting to receive from a closed channel
545
*/
546
class ClosedReceiveChannelException(message: String? = null) : NoSuchElementException(message)
547
```
548
549
### Completion Handler Exceptions
550
551
Exceptions related to completion handler execution.
552
553
```kotlin { .api }
554
/**
555
* Exception thrown when a completion handler itself throws an exception
556
*/
557
class CompletionHandlerException(
558
message: String,
559
cause: Throwable
560
) : RuntimeException(message, cause)
561
```
562
563
**Usage Context:**
564
565
```kotlin
566
import kotlinx.coroutines.*
567
import kotlinx.coroutines.channels.*
568
569
// Channel exception handling
570
suspend fun handleChannelExceptions() {
571
val channel = Channel<String>()
572
channel.close()
573
574
try {
575
channel.send("message")
576
} catch (e: ClosedSendChannelException) {
577
println("Cannot send to closed channel: ${e.message}")
578
}
579
580
try {
581
val message = channel.receive()
582
} catch (e: ClosedReceiveChannelException) {
583
println("Cannot receive from closed channel: ${e.message}")
584
}
585
}
586
587
// Completion handler exception handling
588
fun handleCompletionExceptions() {
589
val job = Job()
590
591
job.invokeOnCompletion { cause ->
592
// If this handler throws, it wraps in CompletionHandlerException
593
throw RuntimeException("Handler error")
594
}
595
596
job.complete()
597
}
598
```