Exceptions and Error Handling

This section documents Kotlin 2.4.x on the JVM, as published at kotlinlang.org, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against kotlinlang.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Kotlin’s exception model is Java’s Throwable hierarchy at the bytecode level, with two source-level differences: try is an expression, and there are no checked exceptions at all.

try/catch/finally as an Expression

Like if and when (Control Flow), try can produce a value — the value of whichever branch (the try block or a matching catch) actually ran:

fun parseOrDefault(text: String): Int =
    try {
        text.toInt()
    } catch (e: NumberFormatException) {
        0
    } finally {
        println("attempted to parse \"$text\"")   // finally still runs; its value is never the expression's
    }

println(parseOrDefault("42"))   // prints the finally message, then 42
println(parseOrDefault("oops")) // prints the finally message, then 0

Multiple catch blocks work as in Java, matched top-to-bottom against the most specific type first.

No Checked Exceptions

Kotlin has no checked-exception distinction at the language level — every exception behaves like Java’s unchecked (RuntimeException) ones: nothing forces a caller to declare throws or wrap a call in try/catch, even when calling Java code whose method signature does declare checked exceptions:

// Java: void readFile(String path) throws IOException { ... }
fun loadConfig(path: String) {
    readFile(path)     // no throws declaration and no try/catch required, even though the Java method
}                        // declares a checked IOException -- Kotlin simply doesn't enforce it

This was a deliberate design choice: JetBrains observed that Java’s checked exceptions, in practice, tend to be either genuinely handled (which still works fine in Kotlin) or immediately wrapped/rethrown/swallowed as boilerplate — so Kotlin drops the compiler enforcement and leaves the decision of what to catch entirely to the code’s own logic.

Custom Exceptions

Custom exceptions extend Exception (or a more specific existing type) exactly as in Java:

class InsufficientFundsException(
    message: String,
    val shortfall: Double
) : Exception(message)

fun withdraw(balance: Double, amount: Double): Double {
    if (amount > balance) {
        throw InsufficientFundsException(
            "cannot withdraw $amount from balance $balance",
            shortfall = amount - balance
        )
    }
    return balance - amount
}

The Result Type and runCatching

kotlin.Result<T> models a computation that either succeeded (holding a value) or failed (holding a Throwable), as an alternative to throw/catch for error paths that are an ordinary, expected part of a function’s contract rather than an exceptional condition. runCatching { } builds one from a block that may throw:

fun fetchUser(id: Int): Result<User> = runCatching {
    httpClient.get("/users/$id").parseAsUser()    // if this throws, it's captured as Result.failure
}

val result = fetchUser(42)

result
    .onSuccess { user -> println("got ${user.name}") }
    .onFailure { error -> println("failed: ${error.message}") }

val userOrNull: User? = result.getOrNull()          // null on failure, instead of propagating the exception
val userOrDefault = result.getOrDefault(User.GUEST)  // a fallback value on failure

Result is especially useful at API boundaries where a caller should be able to inspect success/failure without setting up a try/catch for every call — it composes with the rest of the functional collection/Sequence API (Collections and Sequences) more naturally than an exception can, since it is an ordinary value rather than a special control-flow event.

See Also