Kotlin Exceptions and Error Handling

Handle errors with try, catch and finally, use try as an expression, throw and create your own exceptions, validate with require and check, and wrap risky calls with runCatching and Result.

Lesson 18 of 43Intermediate18 min read

Things go wrong in every real program. A file is missing, a network request times out, a user types "ten" where a number was expected, a calculation divides by zero. When Kotlin hits a problem it can't continue from, it throws an exception: an object describing what went wrong. If nothing catches it, the program stops and prints the error.

Kotlin's exception handling will look familiar if you know Java, with two differences. First, try is an expression, so it can produce a value directly. Second, Kotlin has no checked exceptions: the compiler never forces you to catch or declare exceptions, so it's up to you to handle the ones that matter.

In this lesson you'll catch and handle exceptions, clean up with finally, throw your own, validate inputs with require and check, and use runCatching and Result for a more functional style.

try, catch and finally

Kotlin
try {
    val n = "abc".toInt()
    println(n)
} catch (e: NumberFormatException) {
    println("Not a number: ${e.message}")
} finally {
    println("Done")
}
  • Code that might fail goes in try.
  • catch (e: Type) handles that kind of exception; e.message describes it.
  • You can have several catch blocks. Put specific types first and general ones, such as Exception, last.
  • finally always runs, whether or not an exception happened, which makes it the place for clean-up.

try as an expression

Because try is an expression, the value of the last line in the try or the catch becomes the result:

Kotlin
val age: Int = try {
    input.toInt()
} catch (e: NumberFormatException) {
    0
}

This often removes the need for a var that's assigned in each branch. For simple parsing, though, input.toIntOrNull() ?: 0 is shorter still, which is why Kotlin's OrNull functions are so popular.

throw, custom exceptions, require and check

Throw an exception with throw:

Kotlin
class InsufficientFundsException(val shortBy: Double) :
    Exception("Insufficient funds: short by £$shortBy")

if (amount > balance) throw InsufficientFundsException(amount - balance)

Kotlin's standard library has shortcuts for common checks:

  • require(cond) { "msg" } throws IllegalArgumentException: use it to check arguments.
  • check(cond) { "msg" } throws IllegalStateException: use it to check an object's state.
  • error("msg") throws IllegalStateException immediately.

runCatching and Result

runCatching { } runs a block and wraps the outcome in a Result: either a success holding the value or a failure holding the exception. You then decide what to do without a try-catch block:

Kotlin
val price = runCatching { text.toDouble() }.getOrElse { 0.0 }
val result = runCatching { loadProfile(id) }
result.onSuccess { show(it) }.onFailure { println("Failed: ${it.message}") }

getOrNull() gives null on failure. To close files and other resources safely, even when an exception happens, use .use { }, as the files lesson shows.

Which exception is which

Exceptions form a family tree, and a catch block also catches every exception below the type it names. The ones you'll meet most often:

ExceptionTypical cause
NumberFormatException"80a".toInt()
IllegalArgumentExceptionA bad argument, often from require
IllegalStateExceptionAn object in the wrong state, from check or error
IndexOutOfBoundsExceptionlist[3] on a list with fewer items
NoSuchElementExceptionfirst() on an empty list, getValue with a missing key
NullPointerException!! on a null value
IOExceptionFiles and networks, such as FileNotFoundException

NumberFormatException is a kind of IllegalArgumentException, which in turn is a kind of RuntimeException, so catch (e: IllegalArgumentException) also catches a failed toInt().

Unlike Java, Kotlin has no checked exceptions. The compiler never forces you to catch or declare anything, even for file errors. That keeps code short, but it means you have to think about which failures are likely and handle them where it makes sense.

Wrapping an exception without losing the cause

When a low-level error should become a clearer, higher-level one, pass the original exception as the cause. The message explains what your code was trying to do, and the cause keeps the technical detail for debugging:

Kotlin
class ConfigException(message: String, cause: Throwable? = null) :
    Exception(message, cause)

fun readPort(text: String): Int =
    try {
        text.toInt()
    } catch (e: NumberFormatException) {
        throw ConfigException("Bad port: $text", e)
    }

Calling readPort("80a") throws a ConfigException with the message "Bad port: 80a", and its cause is the original NumberFormatException, whose message is For input string: "80a". Stack traces print both, so nothing is lost.

One detail about finally: it runs even when the try block returns early, but its value is ignored. In val x = try { 5 } finally { 10 }, x is 5. Keep finally for clean-up, never for producing a result.

Kotlin Exceptions Syntax

Exception handling

try { risky() } catch (e: IOException) { } catch (e: Exception) { } finally { }
val x = try { parse() } catch (e: Exception) { default }
throw IllegalArgumentException("message")
class MyError(msg: String) : Exception(msg)
require(age >= 0) { "age must not be negative" }
check(isOpen) { "connection is closed" }
runCatching { risky() }.getOrElse { fallback }

Kotlin Exceptions Summary

  • try, catch and finally handle exceptions; finally always runs.
  • try is an expression, so it can return a value from try or catch.
  • throw raises an exception; custom exceptions extend Exception.
  • require checks arguments, check verifies state, error throws immediately.
  • runCatching returns a Result; use getOrElse, getOrNull, onSuccess and onFailure.

Kotlin Exceptions Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Parsing input safely

Kotlin
fun main() {
    val inputs = listOf("42", "ten", "7")
    for (text in inputs) {
        val n: Int = try {
            text.toInt()
        } catch (e: NumberFormatException) {
            println("'$text' is not a number")
            0
        }
        println("Doubled: ${n * 2}")
    }
}
Output
Doubled: 84
'ten' is not a number
Doubled: 0
Doubled: 14
Line-by-line explanation
  1. Lines 4 to 9try is an expression: the value of whichever block runs is assigned to n.
  2. Lines 7 to 8The catch block prints a message, then its last expression, 0, becomes the value.
  3. Line 10The loop carries on after an error, because each exception is caught inside it.
Example 2

A custom exception and finally

Kotlin
class InsufficientFundsException(shortBy: Double) :
    Exception("Insufficient funds: short by £$shortBy")

class Account(var balance: Double) {
    fun withdraw(amount: Double) {
        require(amount > 0) { "Amount must be positive" }
        if (amount > balance) throw InsufficientFundsException(amount - balance)
        balance -= amount
    }
}

fun main() {
    val acc = Account(100.0)
    for (amount in listOf(30.0, 120.0, -5.0)) {
        try {
            acc.withdraw(amount)
            println("Withdrew $amount")
        } catch (e: InsufficientFundsException) {
            println(e.message)
        } catch (e: IllegalArgumentException) {
            println("Invalid: ${e.message}")
        } finally {
            println("Balance: ${acc.balance}")
        }
    }
}
Output
Withdrew 30.0
Balance: 70.0
Insufficient funds: short by £50.0
Balance: 70.0
Invalid: Amount must be positive
Balance: 70.0
Line-by-line explanation
  1. Lines 1 to 2A custom exception passes a helpful message to Exception.
  2. Line 6require throws IllegalArgumentException for bad arguments.
  3. Lines 18 to 21Specific exception types get specific messages.
  4. Lines 22 to 24finally runs every time, success or failure.
Example 3

runCatching and Result

Kotlin
fun loadScore(name: String): Int {
    if (name.isBlank()) error("Name is empty")
    return name.length * 10
}

fun main() {
    val ok = runCatching { loadScore("Ada") }
    val bad = runCatching { loadScore("  ") }

    println(ok.getOrNull())
    println(bad.getOrNull())
    println(bad.getOrElse { -1 })

    bad.onFailure { println("Failed: ${it.message}") }
    println(ok.isSuccess)
}
Output
30
null
-1
Failed: Name is empty
true
Line-by-line explanation
  1. Line 2error throws an IllegalStateException with the message.
  2. Lines 7 to 8runCatching never throws; it wraps the outcome in a Result.
  3. Lines 10 to 12Read the value, null, or a fallback.
  4. Line 14onFailure runs only when there was an exception.

Common Kotlin Exceptions Mistakes

Catching Exception and ignoring it

Don't
try {
    saveOrder(order)
} catch (e: Exception) { }        // the order silently isn't saved
Do
try {
    saveOrder(order)
} catch (e: IOException) {
    showMessage("Couldn't save. Please try again.")
    log(e)
}

General catch before specific

Don't
catch (e: Exception) { ... }
catch (e: NumberFormatException) { ... }   // never reached
Do
catch (e: NumberFormatException) { ... }
catch (e: Exception) { ... }

Exceptions for normal results

Don't
fun findUser(id: Int): User = users[id] ?: throw Exception("not found")
Do
fun findUser(id: Int): User? = users[id]

A missing user is an expected outcome, so return null and let the caller decide.

Kotlin Exceptions in Real Projects

Exceptions in Kotlin apps

Android apps catch network and database exceptions to show friendly messages instead of crashing, and report unexpected ones to crash-reporting tools such as Firebase Crashlytics. Repositories often convert exceptions into sealed result types or Result objects, so the UI handles success and failure explicitly. In coroutines, exceptions follow structured rules covered in the coroutines lesson.

Kotlin Exceptions FAQ

Does Kotlin have checked exceptions?

No. Kotlin treats every exception as unchecked, so you're never forced to catch one or list it in a throws clause. If Java code calls your Kotlin function and needs to know it can throw, add @Throws(IOException::class) to the function.

How do I catch multiple exception types in one block in Kotlin?

Kotlin has no catch (A | B e) syntax. Either write separate catch blocks, or catch a shared parent type and branch inside it with when (e) { is NumberFormatException -> ...; is IllegalStateException -> ... }, rethrowing anything you didn't expect with else -> throw e.

What is the difference between require and check in Kotlin?

require validates arguments passed into a function and throws IllegalArgumentException, meaning the caller made a mistake. check validates the object's own state and throws IllegalStateException, meaning it was used at the wrong time, such as withdrawing from a closed account. Both take a lambda that builds the error message.

Should I use runCatching or try-catch?

Use try-catch when you know which exception you expect and want to handle it right there. runCatching catches everything, which suits passing a success or failure along as a value, but it also hides bugs you didn't anticipate. In coroutine code it even catches cancellation, so be careful using it around suspending calls.

How do I print the stack trace of an exception in Kotlin?

Call e.printStackTrace() to print it to standard error, or e.stackTraceToString() to get it as a String for logging. In real apps, pass the exception to your logging library, as in logger.error("Save failed", e), so the full trace and any cause are recorded.

Kotlin Exceptions Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the code so the clean-up message always prints.

try {
    work()
} catch (e: Exception) {
    println("Failed")
}  {
    println("Cleaned up")
}
Predict the outputeasy

What does this print?

val x = try { "5".toInt() } catch (e: Exception) { -1 }
println(x)
Multiple choicemedium

Which exception does require(false) { "bad" } throw?

Predict the outputmedium

What does this print?

val r = runCatching { listOf(1, 2)[5] }
println(r.getOrElse { 0 })
Multiple choicehard

Does Kotlin force you to catch exceptions that a function might throw?

Kotlin Exceptions Quiz

5 questions to check what you learned.

  1. What does finally do?

    • Always runs after try and any catch
    • Runs only when there's no exception
    • Runs only when there is an exception
    • Ends the program
  2. What does check(isConnected) { "Not connected" } do when isConnected is false?

    • Returns false
    • Throws IllegalStateException with the message
    • Prints the message
    • Connects automatically
  3. What does runCatching { } return?

    • The value or null
    • A Boolean
    • It throws if the block fails
    • A Result that holds either the value or the exception
  4. How do you create a custom exception?

    • exception MyError
    • object MyError : Error
    • class MyError(msg: String) : Exception(msg)
    • fun MyError() = throw
  5. Why put specific catch blocks before general ones?

    • Catch blocks are checked in order, so a general Exception first would catch everything and the specific block would never run
    • For speed
    • The compiler requires alphabetical order
    • So finally runs first