or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

channels.mdcoroutine-builders.mddispatchers.mdexception-handling.mdflow-api.mdindex.mdjobs-deferreds.mdstructured-concurrency.mdsynchronization.md

exception-handling.mddocs/

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

```