---
title: 'Exception Handling'
source: 'https://academia.sh/en/courses/kotlin/exception-handling'
course: 'The Kotlin Language'
language: en
updated: '2026-10-21T05:02:39+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that Kotlin does not require declaring exceptions and that try is an expression that produces a value'
    - 'Carries an error as a value with runCatching, Result, getOrElse and exceptionOrNull'
    - 'Shows that inside coroutineScope the failure of one sibling cancels the other and propagates outward'
    - 'Measures that two exceptions built with the same message are not counted as equal, and that a data class exception is compared by content'
---

# Exception Handling

Measures the unchecked exception model, the try expression, Result and propagation in a coroutine, shows that two exceptions with the same message are not counted as equal, and closes the course.

The previous lesson measured that two flows are not counted as equal. This last lesson asks, instead of a value, whether an error is equal: are two exceptions that carry the same message equal? First the error model of Kotlin is read. In Java a function declares with `throws` which exceptions it may throw, and the caller has to catch them. In Kotlin there is no such declaration; all exceptions count as **unchecked exceptions**, and catching is up to the caller. **Throwing** is done with `throw`, **catching** with `try` and `catch`. The measuring question is again the same: **what makes these two values equal?**

```kotlin
// Criterion.kt — classifies the criterion that makes two values equal
class Criterion(val label: String, first: Any?, twin: Any?, other: Any?) {
    val sameObject = first === twin
    val equalTwin = first == twin
    val equalOther = first == other

    val name: String = when {
        sameObject -> "singleton"
        !equalTwin -> "identity"
        !equalOther -> "content"
        else -> "partial-content"
    }

    fun report() {
        println("$label: same-object=$sameObject, twin=$equalTwin, other=$equalOther -> $name")
    }
}

fun summarize(checks: List<Criterion>) {
    for (name in listOf("singleton", "identity", "content", "partial-content")) {
        println("$name: ${checks.count { it.name == name }}")
    }
}
```

## The Unchecked Exception and the try Expression

The block below measures two things. `readDisk` throws Java's `IOException`, but there is no declaration in its signature and the compiler asks for nothing. `try`, on the other hand, is an expression: the block gives the value of its last expression, the `catch` branch gives its own value, and `finally` runs in every case but produces no value. The log list shows the order.

```kotlin
// Main.kt — unchecked exception, try expression and finally
import java.io.IOException

fun readDisk(): String {
    throw IOException("disk")
}

fun withdraw(balance: Int, amount: Int): Int {
    if (amount > balance) throw IllegalStateException("insufficient")
    return balance - amount
}

fun main() {
    val log = mutableListOf<String>()
    val left = try {
        log.add("try")
        withdraw(100, 500)
    } catch (e: IllegalStateException) {
        log.add("catch")
        -1
    } finally {
        log.add("last")
    }
    println(left)
    println(log)
    try {
        readDisk()
    } catch (e: IOException) {
        println(e::class.simpleName)
    }
    println(withdraw(100, 30))
}
```

```
-1
[try, catch, last]
IOException
70
```

The first line is `-1`: because `withdraw` threw, the value of the `try` block was discarded and the `-1` of the `catch` branch was assigned to `left`. The log tells the order `[try, catch, last]`; `finally` ran last, after the value was settled. The third line gives the name `IOException`; it was caught because a `catch` was written, and the compilation would have passed even if it had not been. The last line shows that the path that does not throw returns `70`.

**CC35.** These measurements are for `try` expressions with a single thread and no `return`. The result of a `return` or a new exception thrown from inside `finally`, closing a resource with `use`, and the `Error` classes were not measured.

## runCatching and Result

To carry an error as a value instead of catching it, `runCatching { }` is used. If the block returned, the `Result` is successful; if it threw, it is failed. `getOrElse` gives a fallback value on failure, and `exceptionOrNull` gives `null` if successful and the exception itself if not. First, **an exception class of its own** is defined in a helper file; the main block builds a function that throws it.

```kotlin
// Errors.kt — an exception class of its own
class LimitException(val limit: Int) : Exception("limit exceeded")
```

```kotlin
// Main.kt — runCatching and Result
fun parse(text: String): Int {
    if (text.length > 3) throw LimitException(3)
    return text.toInt()
}

fun main() {
    val ok = runCatching { parse("42") }
    val badFormat = runCatching { parse("x") }
    val tooLong = runCatching { parse("12345") }
    println(ok.getOrElse { -1 })
    println(badFormat.getOrElse { -1 })
    println(ok.exceptionOrNull())
    println(badFormat.exceptionOrNull()?.let { it::class.simpleName })
    val error = tooLong.exceptionOrNull()
    if (error is LimitException) {
        println(error.limit)
    }
}
```

```
42
-1
null
NumberFormatException
3
```

Because `ok` is successful, `getOrElse` gave `42`; because `"x".toInt()` threw `NumberFormatException`, the second line is the fallback value `-1`. The third line shows that on a successful result `exceptionOrNull` is `null`, and the fourth line shows the type of the exception on a failed result. The last line reads the `limit` value that the exception class of its own carries: after the `is` check, `error` becomes `LimitException` by smart cast.

**CC36.** These measurements are for `runCatching` calls with an `Int` result and a class under `Exception`. Chains of `map`, `recover` and `onFailure`, `getOrThrow`, and using `Result` as the return type of a function were not measured.

## Propagation in a Coroutine

In structured concurrency, if a child coroutine fails, the scope is cancelled: the siblings receive the cancellation and the exception comes out of `coroutineScope`. In the block below the first child will wait a thousand milliseconds and the second will throw at once. Only the type of the exception caught outside is printed, not its message.

```kotlin
// Main.kt — propagation of an exception in a coroutine
import kotlinx.coroutines.*

fun main() = runBlocking {
    val log = mutableListOf<String>()
    val caught = try {
        coroutineScope {
            launch {
                try {
                    delay(1000)
                    log.add("done")
                } finally {
                    log.add("cancelled")
                }
            }
            launch {
                throw IllegalStateException("sibling")
            }
        }
        "none"
    } catch (e: Exception) {
        e::class.simpleName
    }
    println(caught)
    println(log)
}
```

```
IllegalStateException
[cancelled]
```

The exception caught outside is the `IllegalStateException` that the child threw. In the log there is only `cancelled`: the cancellation reached the waiting child, the `finally` block ran, and `done` was never written. The `"none"` line after the `coroutineScope` call was never reached. The exception goes past the scope of the coroutine and can be caught with an ordinary `try`.

**CC37.** These measurements are for a single `coroutineScope`, a single `runBlocking` and one failed child. `supervisorScope`, `CoroutineExceptionHandler`, and how the exceptions of several failed children are gathered were not measured.

## The Criterion: Identity

Now the real question. Are two exceptions built with the same message equal? The `Throwable` class does not define `equals`, so it falls back to identity. The bounding measurements add `Result` and an exception written as a data class. `Result.failure` carries the exception inside: two separate exceptions make two separate `Result`s, and the same exception makes two `Result`s equal.

```kotlin
// Main.kt — the criterion of an exception and the bounding measurements
data class CodedError(val code: Int) : Exception("code")

fun main() {
    val shared = IllegalStateException("m")
    val checks = listOf(
        Criterion("exception", IllegalStateException("m"), IllegalStateException("m"), IllegalStateException("x")),
        Criterion("result-two-exceptions", Result.failure<Int>(IllegalStateException("m")), Result.failure<Int>(IllegalStateException("m")), Result.failure<Int>(IllegalStateException("x"))),
        Criterion("result-same-exception", Result.failure<Int>(shared), Result.failure<Int>(shared), Result.failure<Int>(IllegalStateException("x"))),
        Criterion("result-success", Result.success(1), Result.success(1), Result.success(2)),
        Criterion("data-class-exception", CodedError(7), CodedError(7), CodedError(8)),
    )
    for (check in checks) check.report()
    summarize(checks)
}
```

```
exception: same-object=false, twin=false, other=false -> identity
result-two-exceptions: same-object=false, twin=false, other=false -> identity
result-same-exception: same-object=false, twin=true, other=false -> content
result-success: same-object=false, twin=true, other=false -> content
data-class-exception: same-object=false, twin=true, other=false -> content
singleton: 0
identity: 2
content: 3
partial-content: 0
```

The first line is the criterion of this lesson: two exceptions with the same message are not equal, and the criterion is **identity**. On the second line the `Result`s that wrap two separate exceptions are not equal either. On the third line two separate `Result` objects that wrap the same exception are equal: `Result` inherits the criterion of the exception. `Result.success(1)` gives content twice. `CodedError`, written as a data class, is equal with the same code, because the generated `equals` redefines the identity equality inherited from `Exception`. The bounding measurement is this: **the criterion of an exception is decided by its type, not by its message.**

**CC38.** These measurements are for `IllegalStateException` and `Result`. The `cause` chain of an exception, the contents of `stackTrace`, and other exception classes that have their own `equals` were not measured.

## Summary

- Kotlin has no `throws`; all exceptions are unchecked exceptions. `try` is an expression that produces a value, and `finally` runs last.
- `runCatching` carries the error as a `Result`; `getOrElse` gives a fallback value, and `exceptionOrNull` gives the exception.
- Inside `coroutineScope`, when one child fails, the siblings are cancelled and the exception propagates outward.
- Two exceptions with the same message are not equal. The criterion of this lesson: **identity**.
- The bounding measurement: `Result.failure` carries the criterion of the exception; an exception written as a data class is content with the same fields.

## Course Wrap-Up

In twenty-three lessons the course asked a single question, and the criterion of each lesson was taken from the summary of the source lesson:

| Lesson | Measured value | Criterion | Bounding measurement |
|---|---|---|---|
| Kotlin's Place | data class, JVM list | content | not equal if `name` in the constructor differs |
| Variables | `const val` constant | singleton | the same text built at run time is content |
| Basic Types | boxed `1000` | content | 127 is singleton; NaN changes with the static type |
| String Templates | two separate templates | content | a template with one variable is singleton |
| Control Flow | `when` branch, plain class | identity | `is Box` matches; two `1..3` are content |
| Functions | two `::plain` | content | the `listOf()` default is singleton |
| Lambdas and Higher-Order Functions | two separate lambdas | identity | a non-capturing lambda is singleton; `::inc` is content |
| Scope Functions | `let` with `copy` | content | the twin of `also` is singleton and spoils the measurement |
| Null Safety | `null` with `null` | singleton | two filled strings are content; filled with `null` is identity |
| Safe Calls and the Elvis Operator | two safe calls | content | on an empty receiver two `null`s are singleton |
| Classes and Constructors | class without `equals` | identity | the same pattern is content, partial content by `equals` |
| Data Classes | object built with `copy` | content | an `IntArray` field is identity, a `List` field is content |
| Sealed Types and Enumerations | enumeration entry | singleton | the kind of the subtype decides the criterion |
| Object Declarations | two reads | singleton | an object expression is new each time, identity |
| Interfaces and Delegation | two objects delegating with `by` | identity | two data classes with the same delegate are equal |
| Extension Functions | extension `equals` | identity | the member beats the extension |
| Generics and Variance | two empty lists, two type parameters | singleton | a generic data class looks at the field value |
| Collection Types | list equality | content | set order is partial content; the empty list is singleton |
| Transformation Operations | two `map`s | content | `distinct` uses the criterion of the element |
| Sequences | two `sequenceOf`s | identity | content with `toList()` |
| Coroutines | two `Deferred`s | identity | `await` results are content |
| Flows | `distinctUntilChanged` | content | with a plain class 5 values remain; the flow is identity |
| Exception Handling | two exceptions with the same message | identity | `Result` carries the criterion of the exception; a data class exception is content |

The distribution of the twenty-three lessons: singleton 5, identity 8, content 10, partial content 0. Partial content was not the criterion of any lesson; it appeared only in the bounding measurements. The core of the opening lesson is a separate count: the distribution of its eight values is 2/2/3/1 (singleton/identity/content/partial content). The two are not the same thing, because the core counts eight values and the table counts twenty-three lessons.

That two exceptions built with the same message do not come out equal gathers the whole answer of the course: in Kotlin, equality is given not by how a value looks but by the rule of its type. The next course is Android Platform and Tools; it covers the parts of an app and the life cycles of those parts.
