Handle failure and narrow types safely. try/catch/finally (and try as an expression), throwing and defining exceptions, and the is / as / as? operators with smart casts that make casting safe.
Kotlin handles errors with try/catch/finally like Java, but with one twist: try is an expression, so it can return a value you assign directly. You catch specific exception types to react appropriately, and finally runs no matter what — for cleanup that must always happen. Catching specific types rather than a blanket Exception keeps error handling honest.
fun parseOrDefault(text: String): Int {
// try is an EXPRESSION — its result is assigned directly
return try {
text.toInt() // may throw NumberFormatException
} catch (e: NumberFormatException) {
-1 // the value when parsing fails
} finally {
println("parse attempt done")// always runs (success, failure, or return)
}
}
fun main() {
println(parseOrDefault("42")) // 42
println(parseOrDefault("oops")) // -1
}You raise an error with throw, and you define your own exception by extending Exception (or a subclass) to model a domain-specific failure with a clear name. Meaningful exception types let callers catch exactly what they can handle. Because throw is an expression of type Nothing, it can be used on the right of an Elvis operator to fail fast when a value is missing.
// a domain-specific exception reads better than a generic one
class InsufficientFundsException(val shortfall: Double) :
Exception("Short by $shortfall")
fun withdraw(balance: Double, amount: Double): Double {
if (amount > balance) throw InsufficientFundsException(amount - balance)
return balance - amount
}
// throw has type 'Nothing', so it works after Elvis to fail fast:
fun requireUser(name: String?): String =
name ?: throw IllegalArgumentException("name required")
fun main() {
try { withdraw(50.0, 80.0) }
catch (e: InsufficientFundsException) { println(e.message) } // Short by 30.0
}The is operator tests whether a value is of a given type, and !is is its negation. Kotlin then applies a smart cast: inside the branch where is succeeded, the compiler treats the value as that type, so you use it directly without an explicit cast. Combined with when over a sealed type, this is how you handle each concrete type cleanly.
fun describe(x: Any): String = when (x) {
is String -> "string of length ${x.length}" // smart-cast to String
is Int -> "int doubled = ${x * 2}" // smart-cast to Int
is List<*> -> "list of ${x.size} items" // smart-cast to List
else -> "unknown"
}
fun main() {
println(describe("hello")) // string of length 5
println(describe(21)) // int doubled = 42
}
// after 'is' succeeds, no explicit cast is needed — the compiler knows the type.When you must convert a type explicitly, as performs an unsafe cast that throws ClassCastException if it is wrong, while as? performs a safe cast that returns null instead. Prefer as? combined with the Elvis operator so a wrong type degrades gracefully rather than crashing — the same null-safety mindset applied to casting.
fun main() {
val obj: Any = "hello"
val s1 = obj as String // unsafe: throws ClassCastException if wrong type
val n1 = obj as? Int // safe: returns null instead of throwing -> Int?
val n2 = obj as? Int ?: 0 // safe cast + default -> 0
println(s1.uppercase()) // HELLO
println(n1) // null (obj isn't an Int)
println(n2) // 0
}
// prefer 'as?' + Elvis over 'as' — a wrong type degrades to null, not a crash.