Exceptions and error handling

Kotlin · Practical · 10 min

Choosing between exceptions, nullable returns and Result types shapes robust Kotlin APIs.

Kotlin uses exceptions like Java, with one big difference: there are no checked exceptions. You're never forced to catch or declare them (@Throws exists only for Java callers).

try is an expression:

val n = try { s.toInt() } catch (e: NumberFormatException) { 0 }

throw is an expression of type Nothing, so it can be used in Elvis expressions: val user = find(id) ?: throw NotFoundException(id). finally runs regardless. For resources, use { } closes any Closeable automatically, like try-with-resources.

Preconditions from the standard library:

  • require(cond) { "message" } throws IllegalArgumentException for bad arguments.
  • check(cond) throws IllegalStateException for bad state.
  • requireNotNull(x) / checkNotNull(x) return the non-null value; error("msg") throws IllegalStateException.

Alternatives to throwing:

  • Return null for "not found" or "can't parse" (toIntOrNull).
  • Return a sealed result type for domain errors with data.
  • runCatching { } wraps a block into Result<T>, with getOrNull(), getOrElse { }, onFailure, map. Note it catches everything, including coroutine CancellationException, so avoid it around suspending code.

Use exceptions for programming errors and truly exceptional failures; use types for expected outcomes callers must handle.

Example

import java.io.StringReader

fun parsePort(s: String): Int {
    val n = s.toIntOrNull() ?: throw IllegalArgumentException("not a number: $s")
    require(n in 1..65535) { "port out of range: $n" }
    return n
}

fun main() {
    val port = try { parsePort("80x") } catch (e: IllegalArgumentException) {
        println(e.message)                  // not a number: 80x
        8080
    }
    println(port)                           // 8080

    val r: Result<Int> = runCatching { parsePort("70000") }
    println(r.isFailure)                    // true
    println(r.exceptionOrNull()?.message)   // port out of range: 70000
    println(runCatching { parsePort("443") }.getOrElse { -1 }) // 443

    val first = StringReader("line1\nline2").buffered().use { it.readLine() }
    println(first)                          // line1 (reader closed)
    var started = false
    runCatching { check(started) { "not started" } }.onFailure { println(it.message) } // not started
}

Remember

  • No checked exceptions; try and throw are expressions
  • require for arguments, check for state, error() to fail
  • use { } closes resources
  • Return null, sealed results or Result for expected failures

Watch out for

  • Wrapping suspending code in runCatching, which swallows coroutine cancellation.

Check yourself

  1. Which exception does require(x > 0) throw when x is 0?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: kotlinlang.org/docs/exceptions.html