Error Handling in Kotlin: try/catch and Beyond
Error handling is an essential part of writing reliable and robust code. In Kotlin, like in Java, there is a classic try/catch/finally mechanism, but the language also offers more modern, functional approaches. In this article, we will thoroughly examine all ways of handling exceptions in Kotlin: from basic try/catch to elegant runCatching and powerful Result and sealed class.
1. Classic try/catch/finally
The basic syntax for exception handling in Kotlin is almost identical to Java, but with one important difference: try can be an expression, meaning it can return a value.
Example 1: try as an expression
fun parseIntOrNull(input: String): Int? { return try { input.toInt() } catch (e: NumberFormatException) { null }}
fun main() { val result = parseIntOrNull("123") println(result) // 123
val badResult = parseIntOrNull("abc") println(badResult) // null}Example 2: Multiple catch blocks
fun processInput(input: String) { try { val number = input.toInt() val result = 100 / number println("Result: $result") } catch (e: NumberFormatException) { println("Error: invalid number format") } catch (e: ArithmeticException) { println("Error: division by zero") } finally { println("The finally block always executes") }}The finally block executes in any case — both on successful execution and when an exception occurs. This is convenient for releasing resources (closing files, connections).
2. Elvis Operator (?:) and Safe Calls
Kotlin offers an elegant way to handle null values, which often helps avoid cumbersome try/catch constructs.
Example: Safe conversion with the Elvis operator
fun safeStringToInt(input: String): Int { return input.toIntOrNull() ?: 0 // If null, return 0}
fun main() { println(safeStringToInt("42")) // 42 println(safeStringToInt("abc")) // 0}The toIntOrNull() function is a built-in Kotlin alternative to try/catch. It does not throw an exception but returns null on error. The Elvis operator ?: substitutes a default value.
3. Functional Approach: runCatching and Result
Modern Kotlin recommends using a functional style for error handling. The key tools are runCatching and the Result class.
Example: runCatching
import kotlin.Result
fun divide(a: Int, b: Int): Result<Int> { return runCatching { a / b }}
fun main() { val successResult = divide(10, 2) val failureResult = divide(10, 0)
// Handling the result successResult .onSuccess { println("Success: $it") } // Success: 5 .onFailure { println("Error: ${it.message}") }
failureResult .onSuccess { println("Success: $it") } .onFailure { println("Error: ${it.message}") } // Error: / by zero
// Getting a value with a default val value1 = successResult.getOrDefault(-1) // 5 val value2 = failureResult.getOrDefault(-1) // -1
// Transforming the result val mappedResult = successResult.map { it * 2 } // Result(10)}Advantages of runCatching:
- The code becomes declarative and readable.
- No need to manually write
try/catch. - The result can be easily combined, transformed, and processed with call chains (
.map