try, catch, and finally
When something goes wrong at runtime — dividing by zero, parsing a malformed number, an account with insufficient funds — a Kotlin program can either crash immediately or handle the problem gracefully. The try, catch, and finally block is Kotlin’s core mechanism for catching and recovering from these runtime errors. Unlike Java, Kotlin has no checked exceptions and no throws clause to satisfy, and — perhaps surprisingly to Java developers — try itself can be used as an expression that produces a value.
Overview: How try, catch, and finally work
Every runtime error in Kotlin is represented by an object that is a subtype of Throwable. Throwable has two main branches: Error, for serious problems the program usually shouldn’t try to recover from (like OutOfMemoryError), and Exception, for problems a program can reasonably catch and handle. Most exceptions you write and catch — ArithmeticException, NumberFormatException, IllegalStateException, and your own custom exceptions — extend RuntimeException, a subtype of Exception.
A key difference from Java: Kotlin has no checked exceptions. In Java, a method that throws IOException must declare throws IOException, and every caller is forced by the compiler to either catch it or re-declare it. Kotlin drops this entirely — every exception in Kotlin behaves like Java’s unchecked RuntimeException. You are never forced to catch anything, and functions never need a throws-style declaration. This is a deliberate design choice: the Kotlin team found checked exceptions in large Java codebases tended to produce either meaningless boilerplate (an empty catch (e: Exception) {}) or exceptions being wrapped and rethrown for no real benefit.
The other big difference is that try is an expression, not just a statement. Just like if and when, a try block can produce a value that you assign to a variable or return directly. The value of a try expression is the value of the last expression evaluated in whichever branch actually ran — the try body if nothing was thrown, or the matching catch body if an exception was caught. The finally block never contributes a value to the expression; its only job is to run cleanup code unconditionally.
Because throw is itself an expression with the special type Nothing (a type with no instances, meaning “this code never returns normally”), the compiler can slot a throw into almost any expression position — including as one branch of a try expression or after the Elvis operator (?:) — without breaking type checking.
Finally, Kotlin has no dedicated try-with-resources syntax like Java 7+. Instead, any Closeable resource (files, streams, sockets) exposes a standard library extension function, use { ... }, that runs a lambda and guarantees the resource is closed afterward, exception or not — it is the idiomatic replacement for a manual try/finally around close().
Syntax
try {
// code that might throw an exception
} catch (e: SpecificExceptionType) {
// handle a specific exception
} catch (e: AnotherExceptionType) {
// handle a different exception
} finally {
// always runs, exception or not
}
- try — mandatory. Wraps the code that might throw.
- catch (e: Type) — zero or more clauses. Each declares the exception type it handles;
eis a normal local variable of that type inside the block. At least onecatchor afinallymust be present. - Catch order matters — clauses are checked top to bottom, and the first one whose type matches (via subtype checking) runs. Put more specific exception types before more general ones.
- finally — optional. Runs after the try/catch has finished, whether an exception was thrown, caught, uncaught, or the block returned early. Used for cleanup: closing files, releasing locks, logging.
Examples
Example 1: Catching an arithmetic error
fun main() {
val a = 10
val b = 0
try {
val result = a / b
println("Result: $result")
} catch (e: ArithmeticException) {
println("Error: ${e.message}")
}
println("Program continues")
}
Output:
Error: / by zero
Program continues
Integer division by zero throws ArithmeticException at runtime (the compiler cannot catch this — it doesn’t know b is zero until the program runs). The catch block matches, prints the error, and — crucially — execution continues normally afterward instead of crashing the program. Note this only applies to integer division; floating-point division by zero produces Infinity or NaN instead of throwing.
Example 2: try as an expression
fun parseNumber(input: String): Int {
return try {
input.toInt()
} catch (e: NumberFormatException) {
println("Could not parse '$input', defaulting to 0")
0
}
}
fun main() {
val a = parseNumber("42")
val b = parseNumber("oops")
println("a = $a, b = $b")
}
Output:
Could not parse 'oops', defaulting to 0
a = 42, b = 0
Here try is used directly after return. When input.toInt() succeeds, that Int becomes the value of the whole try expression. When it throws NumberFormatException, the catch block runs, and its last expression — the literal 0 — becomes the value instead. Both branches must produce a compatible type (here, Int) for this to compile.
Example 3: finally with a custom exception
class InsufficientFundsException(message: String) : Exception(message)
fun withdraw(balance: Int, amount: Int): Int {
if (amount > balance) {
throw InsufficientFundsException("Cannot withdraw $amount, balance is only $balance")
}
return balance - amount
}
fun main() {
val accounts = listOf(100 to 50, 100 to 150)
for ((balance, amount) in accounts) {
try {
val newBalance = withdraw(balance, amount)
println("Withdrew $amount, new balance: $newBalance")
} catch (e: InsufficientFundsException) {
println("Transaction failed: ${e.message}")
} finally {
println("Finished processing withdrawal of $amount")
}
}
}
Output:
Withdrew 50, new balance: 50
Finished processing withdrawal of 50
Transaction failed: Cannot withdraw 150, balance is only 100
Finished processing withdrawal of 150
InsufficientFundsException is a custom exception — any class extending Exception (or one of its subtypes) can be thrown and caught like a built-in one. Notice that finally runs on every iteration regardless of whether the withdrawal succeeded or failed — it always prints “Finished processing withdrawal” right after either the success message or the caught-exception message.
How it works step by step
When the runtime reaches a try block, this is the order of events:
- Statements inside
tryexecute one by one. - If a statement throws, the rest of the
tryblock is skipped immediately, and the runtime looks for the firstcatchclause (checked top to bottom) whose declared type matches the thrown exception or one of its supertypes. - If a matching
catchis found, its body runs. Its last expression becomes the value of the wholetry/catchif it’s being used as an expression. - If no
catchmatches, the exception is left pending — butfinallystill runs before it propagates up the call stack to the caller. finally, if present, always runs last — after a successfultry, after a caught exception, or after an uncaught one — even if thetryorcatchblock contains areturn.- If
finallyitself contains areturn,throw, orbreak/continue, that outcome replaces whatever thetryorcatchblock was about to do — including silently discarding a pending exception. This surprising rule is explored in Common Mistakes below.
Common Mistakes
Mistake 1: Returning from finally silently swallows exceptions
Wrong:
fun riskyOperation(): Int {
try {
throw RuntimeException("Something went wrong")
} finally {
return -1
}
}
fun main() {
println("Result: ${riskyOperation()}")
}
Output:
Result: -1
This compiles and runs without ever mentioning the RuntimeException that was thrown. A return inside finally unconditionally exits the function, discarding any exception that was in flight — the caller has no idea anything went wrong. This is one of the most dangerous gotchas shared by Kotlin and Java: never return (or throw, or break/continue) from a finally block unless you deliberately intend to override the outcome.
Corrected — use finally only for cleanup, and let the exception propagate:
fun riskyOperationFixed(): Int {
try {
throw RuntimeException("Something went wrong")
} finally {
println("Cleaning up resources")
}
}
fun main() {
try {
println("Result: ${riskyOperationFixed()}")
} catch (e: RuntimeException) {
println("Caught: ${e.message}")
}
}
Output:
Cleaning up resources
Caught: Something went wrong
Mistake 2: Catching a general type before a specific one
Wrong — this fails to compile:
open class NetworkException(message: String) : Exception(message)
class TimeoutException(message: String) : NetworkException(message)
fun main() {
try {
throw TimeoutException("Request timed out after 30s")
} catch (e: NetworkException) {
println("Network problem: ${e.message}")
} catch (e: TimeoutException) {
println("Timeout: ${e.message}")
}
}
Since TimeoutException is a subtype of NetworkException, the first catch clause matches every TimeoutException too, making the second clause unreachable. The Kotlin compiler rejects this outright with an error rather than silently letting the second block go dead. The fix is to always order catch clauses from most specific to most general:
open class NetworkException(message: String) : Exception(message)
class TimeoutException(message: String) : NetworkException(message)
fun main() {
try {
throw TimeoutException("Request timed out after 30s")
} catch (e: TimeoutException) {
println("Timeout: ${e.message}")
} catch (e: NetworkException) {
println("Network problem: ${e.message}")
}
}
Output:
Timeout: Request timed out after 30s
Best Practices
- Catch the most specific exception type you can meaningfully handle — avoid catching bare
ExceptionorThrowableunless you are at a top-level boundary (like a server request handler) that must never crash. - Never leave a
catchblock empty. Swallowing an exception silently hides bugs; at minimum, log it. - Prefer
Closeable.use { ... }over a manualtry/finallywhen working with files, streams, or sockets — it closes the resource automatically and re-throws the original exception if closing also fails. - Never
return,throw, orbreakout of afinallyblock — it silently discards whatever exception was propagating. - Don’t use exceptions for expected, normal control flow (like “value not found”); prefer returning a nullable type or a sealed result-style type for those cases, and reserve exceptions for genuinely exceptional situations.
- Define custom exceptions by extending
Exception(notThrowabledirectly) and always pass a descriptivemessageto the superclass constructor. - Keep the code inside a
tryblock as small and focused as possible — wrapping an entire function body makes it hard to know which line could actually throw.
Practice Exercises
- Write a function
safeDivide(a: Int, b: Int): Int?that returns the result ofa / b, ornullinstead of crashing whenbis0. Usetry/catchand make thetryblock the returned expression. - Define a custom exception
InvalidAgeException(message: String)and a functionvalidateAge(age: Int): Intthat throws it whenageis negative or greater than 150, and otherwise returnsage. Call it frommaininside atry/catch/finallyand print a friendly message in each block. - Without running it, predict the exact printed output of a function that has a
tryblock which throws, acatchblock that prints a message and does not return anything, and afinallyblock that also prints a message — then verify your prediction against Example 3 above.
Summary
try/catch/finallyhandles runtime errors represented by subtypes ofThrowable; most exceptions you handle extendRuntimeException.- Kotlin has no checked exceptions — no
throwsdeclarations, and catching is always optional, never enforced by the compiler. trycan be used as an expression: its value is the last expression of whichever branch (try or catch) actually completed.catchclauses are checked top to bottom; order them from most specific to most general or the compiler will reject unreachable clauses.finallyalways runs — success, caught exception, or uncaught exception — making it the right place for cleanup, but areturninside it will silently swallow any pending exception.- Prefer
Closeable.use { ... }over manualtry/finallyfor resource cleanup.
