Skip to content
AgileLogics
7 min read

Exceptions and Error Handling

Prerequisites: Classes and objects, Working with nullable values

When something goes wrong while a program runs — dividing by zero, converting "abc" to a number, a missing file — Kotlin throws an exception. If nothing catches it, the program stops.

Real-world analogy: an exception is a fire alarm. catch is the plan for what to do when it goes off.

try / catch / finally

fun main() {
    val inputs = listOf("42", "abc")

    for (input in inputs) {
        try {
            val n = input.toInt()
            println("Parsed $n")
        } catch (e: NumberFormatException) {
            println("Not a number: $input")
        } finally {
            println("Done with '$input'")
        }
    }
}
Parsed 42
Done with '42'
Not a number: abc
Done with 'abc'
  • catch (e: Type) handles a specific kind of exception.
  • finally runs no matter what — for cleanup.

try is an expression

fun main() {
    val input = "abc"
    val number = try { input.toInt() } catch (e: NumberFormatException) { 0 }
    println(number)
}
0

Throwing, require, and check

fun withdraw(balance: Double, amount: Double): Double {
    require(amount > 0) { "Amount must be positive" }
    if (amount > balance) {
        throw InsufficientFundsException(amount - balance)
    }
    return balance - amount
}

class InsufficientFundsException(val shortfall: Double) :
    Exception("Insufficient funds: short by $shortfall")

fun main() {
    try {
        withdraw(50.0, 80.0)
    } catch (e: InsufficientFundsException) {
        println(e.message)
        println("Please deposit ${e.shortfall}")
    }

    try {
        withdraw(50.0, -5.0)
    } catch (e: IllegalArgumentException) {
        println(e.message)
    }
}
Insufficient funds: short by 30.0
Please deposit 30.0
Amount must be positive
  • require(...) checks arguments (throws IllegalArgumentException).
  • check(...) checks the object's state (throws IllegalStateException).
  • Custom exceptions subclass Exception and can carry extra data.

runCatching and Result

runCatching turns an exception into a Result value:

fun main() {
    val result = runCatching { "12x".toInt() }
    println(result.isFailure)
    println(result.getOrDefault(-1))
    println(runCatching { "12".toInt() }.getOrNull())
}
true
-1
12

All exceptions are unchecked

Unlike Java, Kotlin doesn't force you to declare or catch exceptions. That keeps code shorter but means you decide where failures are handled. For expected, everyday failures ("user not found"), returning null or a sealed result type is often clearer than throwing.

Common mistakes

  • Catching Exception (or Throwable) everywhere and hiding real bugs. Catch the specific types you can handle.
  • Using runCatching around code that runs coroutines — it also catches the cancellation exceptions coroutines rely on (see Level 3).

Practice

Write fun parseAge(text: String): Int that throws IllegalArgumentException with a helpful message if the text isn't a number or isn't between 0 and 150. Call it with "200" in a try/catch and print the message.

Show solution
fun parseAge(text: String): Int {
    val age = requireNotNull(text.toIntOrNull()) { "'$text' is not a number" }
    require(age in 0..150) { "Age $age is out of range (0-150)" }
    return age
}

fun main() {
    try {
        parseAge("200")
    } catch (e: IllegalArgumentException) {
        println("Error: ${e.message}")
    }
}
Error: Age 200 is out of range (0-150)