Exception Handling
Reviewed & published by Brayan K
Kotlin is a modern, concise language that handles errors with try/catch — but with no checked exceptions and a try that's an expression, error handling stays clean and flexible.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll catch exceptions, throw your own, validate with require, and use runCatching.
What You'll Learn in This Lesson
- try / catch / finally basics
- try as a value-returning expression
- Why Kotlin has no checked exceptions
- Throwing exceptions with throw
- Defining custom exception classes
- Validating with require/check and runCatching
1️⃣ try, catch, and finally
Wrap risky code in try; handle failures in catch; run cleanup in finally. Because try is an expression, it can return a value — perfect for parsing with a fallback.
fun main() {
// try/catch handles errors so the program doesn't crash.
try {
val n = "abc".toInt() // throws NumberFormatException
println(n) // never reached
} catch (e: NumberFormatException) {
println("Not a number: ${e.message}")
} finally {
println("This always runs") // cleanup, runs whether or not it threw
}
// try is an EXPRESSION — it can return a value.
val parsed: Int = try {
"42".toInt()
} catch (e: NumberFormatException) {
-1 // fallback value
}
println("Parsed: $parsed") // -> Parsed: 42
}The finally block ran even though the first try threw — that's where you release resources or log, guaranteed.
2️⃣ Throwing, Custom Exceptions, and runCatching
Raise an error with throw, optionally of your own custom exception class carrying extra data. require guards inputs, and runCatching turns a risky call into a Result you can inspect.
// Define a custom exception by extending Exception.
class InsufficientFundsException(val shortfall: Int) :
Exception("Short by $shortfall")
fun withdraw(balance: Int, amount: Int): Int {
// 'throw' raises an exception. require/check are common guards.
require(amount > 0) { "Amount must be positive" }
if (amount > balance) {
throw InsufficientFundsException(amount - balance)
}
return balance - amount
}
fun main() {
println(withdraw(100, 30)) // -> 70
try {
withdraw(100, 150)
} catch (e: InsufficientFundsException) {
println("Declined: ${e.message} (need ${e.shortfall} more)")
}
// runCatching wraps a call into a Result (success or failure):
val result = runCatching { withdraw(50, 80) }
println(result.isFailure) // -> true
println(result.getOrDefault(0)) // -> 0 (fallback)
}runCatching let us treat the outcome as data — checking isFailure and supplying a default — without a single try/catch block.
Your turn. Replace the TODO, then run and compare.
// 🎯 YOUR TURN — replace each TODO then run main()
fun safeDivide(a: Int, b: Int): Int {
return try {
a / b // throws if b == 0
} TODO (e: ArithmeticException) { // 👉 replace TODO with 'catch'
0 // fallback on divide-by-zero
}
}
fun main() {
println(safeDivide(10, 2)) // expect 5
println(safeDivide(10, 0)) // expect 0 (caught)
}Mini-Challenge: A Safe Age Parser
Parse a string to an Int, returning a sentinel value when the input is invalid.
// 🎯 MINI-CHALLENGE: a safe age parser.
// Save as Parse.kt and run main().
//
// 1. Write fun parseAge(input: String): Int that:
// - uses try/catch to call input.toInt()
// - if it throws NumberFormatException, returns -1
// - then, with require, throws if the parsed age is negative
// (require(age >= 0) { "Age cannot be negative" }) — but only
// reach that after a successful parse.
// To keep it simple: return the parsed value, or -1 on bad input.
// 2. In main(), print parseAge("30") and parseAge("oops").
//
// ✅ Example output:
// 30
// -1
fun main() {
// your code here
}Common Errors (and the fix)
- Catching too broadly — catch (e: Exception) hides bugs. Catch the most specific type you expect.
- Swallowing the error silently — An empty catch makes problems invisible. At least log e.message.
- Using exceptions for control flow — Don't throw for ordinary outcomes. Prefer nullable returns or Result.
- Forgetting cleanup — Put resource release in finally, or use use { } for closeable resources.
Pro Tips
- 💡 require / check: use them to validate arguments and state with readable messages.
- 💡 Result over try/catch when you want to pass an outcome around as a value.
- 💡 use { } auto-closes files and streams — the Kotlin equivalent of try-with-resources.
📋 Quick Reference — Exceptions
| Concept | Kotlin Syntax |
|---|---|
| Catch an error | try { } catch (e: E) { } |
| Cleanup | finally { } |
| Throw | throw MyException("...") |
| Validate | require(x > 0) { "..." } |
| Custom exception | class E : Exception() |
| As a Result | runCatching { ... } |
🎉 Lesson Complete!
- ✅ try/catch/finally handle and clean up errors
- ✅ try is an expression that can return a value
- ✅ Kotlin has no checked exceptions
- ✅ throw raises errors; custom classes extend Exception
- ✅ require validates; runCatching gives a Result
Practice quiz
Does Kotlin have checked exceptions like Java?
- Yes, all exceptions are checked
- No, Kotlin has no checked exceptions
- Only for IOException
- Only inside coroutines
Answer: No, Kotlin has no checked exceptions. Kotlin has no checked exceptions; you are never forced to catch or declare any.
What is special about try in Kotlin?
- It must always have a finally block
- It can only wrap one statement
- It is an expression that can return a value
- It cannot catch anything
Answer: It is an expression that can return a value. try is an expression; val x = try { ... } catch (e) { ... } is valid.
When does the finally block run?
- Only when an exception is thrown
- Only when no exception is thrown
- Whether or not an exception was thrown
- Never
Answer: Whether or not an exception was thrown. finally runs whether or not the try block threw, ideal for cleanup.
Which keyword raises an exception?
- throw
- raise
- panic
- fail
Answer: throw. throw raises an exception in Kotlin.
How do you typically define a custom exception?
- Implement the Throwable interface only
- Annotate a function with @Throws
- Use a data class
- Extend Exception (or a subclass)
Answer: Extend Exception (or a subclass). A custom exception extends Exception or a more specific subclass.
What does runCatching return?
- A Result capturing success or failure
- Always the value or null
- Unit
- A Boolean only
Answer: A Result capturing success or failure. runCatching returns a Result<T> capturing the value on success or the exception on failure.
What does require(condition) { message } do when the condition is false?
- Returns null
- Throws IllegalArgumentException with the message
- Logs a warning and continues
- Throws NullPointerException
Answer: Throws IllegalArgumentException with the message. require throws IllegalArgumentException when its condition is false.
For a try-as-expression, which value is produced on success?
- null
- The catch block's value
- Always 0
- The last expression of the try block
Answer: The last expression of the try block. On success the value is the last expression of the try block; on failure, the matching catch block.
Which is the recommended way to release a closeable resource?
- Ignore it; the GC handles it instantly
- Throw at the end
- Put release in finally, or use use { }
- Catch and rethrow
Answer: Put release in finally, or use use { }. Use finally for cleanup, or the use { } function for closeable resources.
What does getOrDefault(0) do on a failed Result?
- Rethrows the exception
- Returns the fallback value 0
- Returns null
- Returns true
Answer: Returns the fallback value 0. getOrDefault returns the supplied fallback when the Result is a failure.
Continue this course
- Previous: Coroutines Intro
- Next: Objects & Companion Objects — Singletons with object and static-like members with companion objects
- Quick reference: Kotlin cheat sheet
Frequently asked questions
Does Kotlin have checked exceptions like Java?
No. In Java, certain exceptions are 'checked', meaning the compiler forces you to either catch them or declare them with throws. Kotlin has no checked exceptions — you are never required to handle or declare any exception. This removes a lot of boilerplate, but it also means you must be deliberate: read documentation to know what can throw, and decide consciously where to handle errors. Every Kotlin exception behaves like a Java unchecked (runtime) exception.
Why is try an expression in Kotlin?
Like if and when, try can produce a value, so you can write val x = try { risky() } catch (e: Exception) { fallback }. The value is the last expression of whichever block runs — the try block on success, or the matching catch block on failure. This lets you handle an error and assign a sensible default in one concise, expression-based statement instead of declaring a mutable variable and writing to it from inside the blocks.
How do I create and throw my own exception?
Define a class that extends Exception (or a more specific subclass), typically passing a message to the superclass and adding any custom properties you need — like a shortfall amount. Then raise it with the throw keyword: throw InsufficientFundsException(50). Custom exceptions make error handling clearer because catch blocks can match your specific type and read your extra data. For simple input validation, the built-in require and check functions throw IllegalArgumentException / IllegalStateException for you.
What is runCatching and the Result type?
runCatching executes a block and returns a Result<T> instead of throwing: it captures success (with the value) or failure (with the exception). You can then inspect it with isSuccess / isFailure, extract a value with getOrNull() or getOrDefault(fallback), or transform it functionally with map and recover. It is a clean, expression-friendly alternative to try/catch when you want to treat the outcome as data and pass it around, rather than branching with control flow.