Exception Handling

Handle runtime errors gracefully with try-catch, finally, throw, custom exceptions, and best practices

try-catch finally throw custom

Table of Contents

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

ExceptionWhen thrown
NullPointerExceptionNull reference used (rare with Kotlin null safety)
ArithmeticExceptionDivision by zero
IndexOutOfBoundsExceptionInvalid list/array index
NumberFormatExceptionInvalid string-to-number conversion
IllegalArgumentExceptionInvalid argument passed to function
IOExceptionFile/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] }
ApproachWhen 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 }
FunctionThrowsTypical use
throw Exception()Any exceptionGeneral explicit throw
require(condition) { }IllegalArgumentExceptionValidate function arguments
check(condition) { }IllegalStateExceptionValidate object state
error("msg")IllegalStateExceptionUnreachable / not implemented
TODO()NotImplementedErrorPlaceholder 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 classUse for
ExceptionRecoverable application errors
RuntimeExceptionProgramming errors (often unchecked in Java)
IllegalArgumentExceptionBad function arguments
IllegalStateExceptionObject in wrong state
IOExceptionFile and network I/O
ErrorSerious 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

SituationRecommended approach
Invalid user inputrequire or return validation error
Network/API failureResult, sealed class, or retry with coroutines
File I/Otry-catch IOException or use { }
Programming bugLet crash in debug; fix root cause
Android UICatch 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

TopicKey Takeaway
Try-CatchHandle exceptions; try-catch is an expression in Kotlin
Finally BlockAlways runs; use use { } for auto-close resources
Throw Keywordthrow, require, check, error
Custom ExceptionsExtend Exception; add fields for error codes/context
Best PracticesCatch specific, log stack traces, use Result/sealed for expected errors
Kotlin noteNo checked exceptions — handle voluntarily
Next lessonFile Handling