Error Handling in Kotlin: try/catch and Beyond — From A to Z
Error handling is one of the key tasks for any developer. In Kotlin, as in other modern languages, there are several approaches to managing exceptional situations. In this article, we will cover all the main methods: from the classic try/catch to advanced techniques using Result, sealed class, and Nothing. You will learn how to write robust and readable code that handles errors correctly.
1. The Classic try/catch/finally Block
The basic mechanism for exception handling in Kotlin is the try/catch block. It works similarly to Java, but with some improvements. In Kotlin, try is an expression that can return a value.
fun parseNumber(input: String): Int? { return try { input.toInt() } catch (e: NumberFormatException) { println("Parsing error: ${e.message}") null // return null on error } finally { println("The finally block always executes") }}
fun main() { val result = parseNumber("123abc") println(result) // null}Key Features:
trycan be used as an expression — the result of the last expression in thetryorcatchblock becomes the result of the entire block.- The
finallyblock always executes (even if there was an exception or areturn). - Kotlin does not have checked exceptions like Java — this simplifies the code.
It is important to remember that overusing try/catch makes code less readable. Use it only for truly exceptional situations, not for flow control.
2. The Result Type and Functional Approach
For more elegant error handling, Kotlin offers the Result<T> type. This is a sealed class that can be either Success (with a result) or Failure (with an exception). It is ideal for a functional style.
import kotlin.Result
fun divide(a: Int, b: Int): Result<Int> { return try { Result.success(a / b) } catch (e: ArithmeticException) { Result.failure(e) }}
fun main() { val result = divide(10, 0) // Handling with when when (result) { is Result.Success -> println("Result: ${result.value}") is Result.Failure -> println("Error: ${result.exception.message}") } // Or using methods result.onSuccess { value -> println("Success: $value") } .onFailure { e -> println("Failure: ${e.message}") } // Transformation val mapped = result.map { it * 2 } // if failure, remains failure println(mapped)}Advantages of Result:
- Explicitly shows that a function can fail.
- Allows method chaining (
map,flatMap,recover). - Does not require
try/catchin the calling code.
However, Result is not intended for storage in collections or passing across coroutine boundaries — for that, it is better to use sealed class.
3. Sealed Classes for Detailed Error Handling
When you need to handle different types of errors differently, sealed class is the ideal choice. You can create a hierarchy of errors and handle them in a when expression with exhaustive coverage.
sealed class NetworkResult<out T> { data class Success<T>(val data: T) : NetworkResult<T>() data class NetworkError(val code: Int, val message: String) : NetworkResult<Nothing>() data class Timeout(val timeout