Exception Handling
Handle runtime errors gracefully with try-catch, finally, throw, custom exceptions, and best practices
try-catch
finally
throw
custom
1Try-Catch
An exception is an event that disrupts normal program flow. Kotlin uses try-catch blocks to handle exceptions — code in try runs normally; if an exception occurs, control jumps to the matching catch block.
Basic try-catchfun divide(a: Int, b: Int): Int {
return try {
a / b
} catch (e: ArithmeticException) {
println("Cannot divide by zero")
0
}
}
fun main() {
println(divide(10, 2)) // 5
println(divide(10, 0)) // Cannot divide by zero → 0
}
Multiple catch blocks
Multiple catch typesfun parseNumber(input: String): Int {
return try {
input.toInt()
} catch (e: NumberFormatException) {
println("Invalid number format: $input")
-1
} catch (e: Exception) {
println("Unexpected error: ${e.message}")
-1
}
}
fun main() {
println(parseNumber("42")) // 42
println(parseNumber("abc")) // Invalid number format → -1
}
try as expression
In Kotlin, try-catch is an expression — the last expression in try or catch is the return value:
try-catch returns a valuefun readIndex(list: List<Int>, index: Int): Int? {
return try {
list[index]
} catch (e: IndexOutOfBoundsException) {
null
}
}
fun main() {
val nums = listOf(10, 20, 30)
println(readIndex(nums, 1)) // 20
println(readIndex(nums, 5)) // null
}
Common exception types
| Exception | When thrown |
NullPointerException | Null reference used (rare with Kotlin null safety) |
ArithmeticException | Division by zero |
IndexOutOfBoundsException | Invalid list/array index |
NumberFormatException | Invalid string-to-number conversion |
IllegalArgumentException | Invalid argument passed to function |
IOException | File/network I/O failures |
Kotlin vs Java
Kotlin has no checked exceptions — you are not forced to declare or catch exceptions like Java's throws IOException. Handle exceptions when it makes sense, not because the compiler requires it.
2Finally Block
The finally block always executes after try and any catch blocks — whether an exception occurred or not. Use it for cleanup: closing files, releasing resources, resetting state.
try-catch-finallyfun processFile(filename: String) {
var resource: AutoCloseable? = null
try {
resource = openResource(filename)
resource.use { /* process data */ }
println("Processing complete")
} catch (e: Exception) {
println("Error: ${e.message}")
} finally {
println("Cleanup — always runs")
resource?.close()
}
}
fun openResource(name: String): AutoCloseable {
return object : AutoCloseable {
override fun close() { println("Resource closed") }
}
}
finally always runs
finally with and without exceptionfun demo(willFail: Boolean) {
try {
if (willFail) throw RuntimeException("Failed!")
println("Success path")
} catch (e: RuntimeException) {
println("Caught: ${e.message}")
} finally {
println("Finally block executed")
}
}
fun main() {
demo(false) // Success → Finally
demo(true) // Caught → Finally
}
use() — idiomatic Kotlin cleanup
Prefer use { } over manual try-finally for Closeable resources — it calls close() automatically:
use extension functionimport java.io.BufferedReader
import java.io.StringReader
fun readLines(input: String): List<String> {
return StringReader(input).use { reader ->
BufferedReader(reader).use { it.readLines() }
}
}
fun main() {
println(readLines("Line1\nLine2")) // [Line1, Line2]
}
| Approach | When to use |
finally { } | Manual cleanup, legacy patterns |
.use { } | Files, streams, DB cursors — preferred in Kotlin |
try-with-resources (Java) | Interop; Kotlin use is equivalent |
Return in finally: Avoid returning from finally — it can override return values or exceptions from try/catch and cause confusing behavior.
3Throw Keyword
Use throw to explicitly raise an exception when a rule is violated or an error condition is detected. Kotlin uses throw as an expression — it has type Nothing (never returns normally).
throw an exceptionfun setAge(age: Int) {
if (age < 0) {
throw IllegalArgumentException("Age cannot be negative: $age")
}
println("Age set to $age")
}
fun main() {
setAge(25) // OK
// setAge(-1) // throws IllegalArgumentException
}
throw vs require / check / error
Kotlin standard helpersfun validateEmail(email: String) {
require(email.contains("@")) { "Invalid email: $email" }
check(email.length <= 100) { "Email too long" }
}
fun notImplemented(): Nothing {
error("Feature not implemented yet")
}
fun main() {
validateEmail("user@example.com") // OK
// validateEmail("invalid") // IllegalArgumentException
}
| Function | Throws | Typical use |
throw Exception() | Any exception | General explicit throw |
require(condition) { } | IllegalArgumentException | Validate function arguments |
check(condition) { } | IllegalStateException | Validate object state |
error("msg") | IllegalStateException | Unreachable / not implemented |
TODO() | NotImplementedError | Placeholder during development |
throw as expression
Elvis with throwfun getConfig(key: String): String {
val value = configMap[key]
return value ?: throw IllegalStateException("Missing config: $key")
}
val configMap = mapOf("host" to "localhost")
fun main() {
println(getConfig("host")) // localhost
}
Prefer Result over throw
For expected failures (network errors, validation), consider returning Result<T> or sealed classes instead of throwing — see Sealed Classes.
4Custom Exceptions
Create custom exception classes by extending Exception or a more specific base class. They communicate domain-specific errors clearly to callers and logs.
Simple custom exceptionclass InvalidCredentialsException(message: String) : Exception(message)
fun login(username: String, password: String) {
if (username.isBlank() || password.isBlank()) {
throw InvalidCredentialsException("Username and password required")
}
if (password.length < 6) {
throw InvalidCredentialsException("Password too short")
}
println("Login successful for $username")
}
fun main() {
try {
login("nikhil", "12345")
} catch (e: InvalidCredentialsException) {
println("Login failed: ${e.message}")
}
}
Custom exception with extra data
Exception with error codeclass ApiException(
val code: Int,
message: String,
cause: Throwable? = null
) : Exception("API Error $code: $message", cause)
fun fetchUser(id: Int): String {
if (id <= 0) {
throw ApiException(400, "Invalid user id: $id")
}
if (id == 999) {
throw ApiException(404, "User not found")
}
return "User#$id"
}
fun main() {
try {
fetchUser(999)
} catch (e: ApiException) {
println("${e.code} — ${e.message}")
}
}
Exception hierarchy
| Base class | Use for |
Exception | Recoverable application errors |
RuntimeException | Programming errors (often unchecked in Java) |
IllegalArgumentException | Bad function arguments |
IllegalStateException | Object in wrong state |
IOException | File and network I/O |
Error | Serious JVM problems — rarely catch |
Sealed result instead of exception (alternative)sealed class NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Failure(val exception: ApiException) : NetworkResult<Nothing>()
}
Naming: End custom exception classes with Exception — e.g. PaymentFailedException, SessionExpiredException.
5Exception Handling Best Practices
Good exception handling keeps apps stable, debuggable, and user-friendly. Follow these habits in Kotlin JVM, Android, and backend projects.
Catch specific exceptions
Avoid bare catch (e: Exception) unless rethrowing or logging at top level.
Meaningful messages
Include context: what failed, input values, and suggested fix.
Don't swallow exceptions
Empty catch blocks hide bugs — at minimum log the stack trace.
Use null safety first
Prevent NPEs with types and ?. before relying on try-catch.
1. Catch specific, not generic
Bad vs good// Avoid — hides all errors
// try { ... } catch (e: Exception) { }
// Prefer — handle known cases
try {
val data = jsonParser.parse(input)
} catch (e: JsonParseException) {
showError("Invalid JSON format")
} catch (e: IOException) {
showError("Network error")
}
2. Log with stack trace
Log exceptions properlyfun safeOperation() {
try {
riskyCall()
} catch (e: Exception) {
// Android: Log.e("AppTag", "Operation failed", e)
println("Error: ${e.message}")
e.printStackTrace()
}
}
fun riskyCall() { /* ... */ }
3. Use Result and sealed classes for expected failures
runCatchingfun fetchData(): Result<String> {
return runCatching {
apiCallThatMightFail()
}
}
fun main() {
fetchData()
.onSuccess { println("Data: $it") }
.onFailure { println("Failed: ${it.message}") }
}
fun apiCallThatMightFail(): String = "response"
4. Fail fast with require/check
Validate earlyclass Account(initialBalance: Double) {
init {
require(initialBalance >= 0) { "Balance cannot be negative" }
}
private var balance = initialBalance
fun withdraw(amount: Double) {
check(amount > 0) { "Withdraw amount must be positive" }
check(balance >= amount) { "Insufficient funds" }
balance -= amount
}
}
5. Decision guide
| Situation | Recommended approach |
| Invalid user input | require or return validation error |
| Network/API failure | Result, sealed class, or retry with coroutines |
| File I/O | try-catch IOException or use { } |
| Programming bug | Let crash in debug; fix root cause |
| Android UI | Catch in ViewModel; expose error state to UI |
Coroutines
In suspend functions, use try-catch around async work or handle errors in CoroutineExceptionHandler — covered in Coroutines.
6Summary Cheatsheet
| Topic | Key Takeaway |
| Try-Catch | Handle exceptions; try-catch is an expression in Kotlin |
| Finally Block | Always runs; use use { } for auto-close resources |
| Throw Keyword | throw, require, check, error |
| Custom Exceptions | Extend Exception; add fields for error codes/context |
| Best Practices | Catch specific, log stack traces, use Result/sealed for expected errors |
| Kotlin note | No checked exceptions — handle voluntarily |
| Next lesson | File Handling |