Error Handling in Kotlin: try/catch and Advanced Techniques

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Advanced Error Handling in Kotlin: from try/catch to Functional Approaches



Error handling is one of the key tasks in developing reliable software. In Kotlin, in addition to the classic try/catch/finally mechanism, there is a whole arsenal of advanced tools: Result, runCatching, sealed class, and functional monads. In this article, we will explore how to go beyond basic usage and make your code safer, more readable, and more predictable.



1. Classic try/catch: When It Is Still Appropriate



The basic try/catch in Kotlin is an expression that can return a value. This is the first thing that distinguishes it from Java.



val result: String = try {    riskyOperation()} catch (e: IOException) {    "I/O error: ${e.message}"} catch (e: Exception) {    "Unknown error: ${e.message}"} finally {    println("The finally block always executes")}


Despite its simplicity, this approach has drawbacks:

  • Exceptions are an expensive operation (creating a stack trace).
  • The error type is not fixed at the type level — it is easy to miss handling.
  • The code becomes cumbersome with a large number of nested operations.


When to use try/catch?

  • When working with external resources (files, network, databases).
  • In library code where exceptions are part of the contract.
  • For critical errors that cannot be handled locally.


2. Functional Approach: Result and runCatching



Kotlin offers a built-in Result class, which represents the result of an operation that can end in success or failure. This is an alternative to exceptions that does not require an expensive stack trace.



fun divide(a: Int, b: Int): Result<Int> {    return runCatching {        a / b    }}

fun main() { val result = divide(10, 0) result.onSuccess { value -> println("Result: $value") }.onFailure { error -> println("Error: ${error.message}") }}


Result Methods

  • getOrNull() — returns the value or null.
  • getOrDefault(default) — returns the value or a default value.
  • map and flatMap — for transformation chains.
  • recover — recovery after an error.


val safeResult = runCatching { riskyOperation() }    .map { it.toUpperCase() }    .recover { "default_value" }    .getOrDefault("fallback")


Important: Result is not intended for passing between threads or storing in class fields — use it only as a local value.



3. Sealed Classes: Modeling Errors at the Type Level



When you need to clearly define all possible states (success, various types of errors), sealed class comes to the rescue. This is a powerful tool for creating algebraic data types.



sealed class NetworkResult<out T> {    data class Success<T>(val data: T) : NetworkResult<T>()    data class Error(val message: String, val code: Int) : NetworkResult<Nothing>()    object Loading : NetworkResult<Nothing>()}

fun fetchUser(id: Int): NetworkResult<User> { return try { val user = api.getUser(id) NetworkResult.Success(user) } catch (e: HttpException) { NetworkResult.Error("Server returned an error", e.code()) } catch (e: Exception) { NetworkResult.Error("Unknown error", 0) }}

fun handleResult(result: NetworkResult<User>) { when (result) { is NetworkResult.Success -> println("User: ${result.data.name}") is NetworkResult.Er

Blogs

Book Recommendations