Phase 5 · Advanced KotlinModule 26~34 min read

Exceptions & Error Handling

Handle failure idiomatically with try as an expression, Result, and sealed error types.

What you'll learn

Things go wrong — bad input, missing files, network failures. Kotlin handles errors with exceptions, but with a modern twist: all exceptions are unchecked, and Kotlin offers functional alternatives (Result, sealed classes) that often read better.

By the end you'll be able to:

  • Understand why Kotlin exceptions are all unchecked
  • Use try/catch as an expression
  • Handle failure functionally with runCatching and Result
  • Model expected errors with sealed classes

All exceptions are unchecked

In Java, checked exceptions force you to declare or catch them, which many find cumbersome. Kotlin made a deliberate choice: every exception is unchecked. There's no throws clause and no compiler requirement to handle anything. You catch what you can meaningfully recover from, and let the rest propagate.

try as an expression

Like if and when, try/catch is an expression in Kotlin — it returns a value. And throw is an expression too (of type Nothing), so it slots into an Elvis or a when branch:

TryCatch.kt
fun parseAge(input: String): Int {
    return try {
        input.toInt()
    } catch (e: NumberFormatException) {
        -1                    // try/catch is an EXPRESSION — it returns a value
    }
}

println(parseAge("25"))   // 25
println(parseAge("abc"))  // -1
Throw.kt
val age = 20

val category = when {
    age < 0  -> throw IllegalArgumentException("Age can't be negative")
    age < 18 -> "minor"
    else     -> "adult"
}
println(category)   // adult

runCatching & Result

For a functional approach, runCatching { ... } runs a block and captures success or failure in a Result object — no try/catch needed. You then extract the value with getOrDefault, getOrNull, or handle both cases with fold:

Result.kt
val good = runCatching { "42".toInt() }
println(good.isSuccess)          // true
println(good.getOrDefault(0))    // 42

val bad = runCatching { "abc".toInt() }
println(bad.getOrDefault(-1))    // -1  (recovered, no crash)

Modeling errors with sealed classes

For expected outcomes (a validation failure, a not-found), exceptions are often the wrong tool — they're for the exceptional. A cleaner approach models success and failure as a sealed class (Module 18), so the compiler forces callers to handle every case with an exhaustive when:

Sealed.kt
sealed class LoadResult
data class Ok(val data: String) : LoadResult()
data class Fail(val error: String) : LoadResult()

fun load(id: Int): LoadResult =
    if (id > 0) Ok("Item $id") else Fail("Invalid id")

when (val r = load(5)) {
    is Ok -> println("Loaded: ${r.data}")
    is Fail -> println("Error: ${r.error}")
}

Key idea

A good rule: use exceptions for truly exceptional, unrecoverable situations, and a sealed result type (or Result) for expected failures you want the caller to handle explicitly. This makes error handling visible and compiler-checked rather than easy to forget.

Recap & quick check

Key takeaways

  • All Kotlin exceptions are unchecked — no throws clause, no forced handling.
  • try/catch is an expression that returns a value; throw is an expression of type Nothing.
  • runCatching { } captures success/failure in a Result — handle it with getOrDefault/getOrNull/fold.
  • Model expected errors with a sealed class so an exhaustive when forces callers to handle every case.
  • Exceptions for the truly exceptional; sealed results/Result for expected failures.

Quick check

1. How do Kotlin exceptions differ from Java's?

2. What is 'try/catch' in Kotlin?

3. What does runCatching { } return?

4. For an expected failure you want callers to handle, what's often better than throwing?

5. When are exceptions the right tool?

Great — your programs fail gracefully now. Next up: Module 27 — Annotations & Reflection.