Обработка ошибок в Kotlin: try/catch и не только — от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Обработка ошибок в Kotlin: try/catch и не только — от А до Я

Обработка ошибок — одна из ключевых задач любого разработчика. В Kotlin, как и в других современных языках, существует несколько подходов к управлению исключительными ситуациями. В этой статье мы разберем все основные методы: от классического try/catch до продвинутых техник с использованием Result, sealed class и Nothing. Вы узнаете, как писать надежный и читаемый код, который корректно обрабатывает ошибки.

1. Классический блок try/catch/finally

Базовый механизм обработки исключений в Kotlin — это блок try/catch. Он работает так же, как в Java, но с некоторыми улучшениями. В Kotlin try — это выражение, которое может возвращать значение.

fun parseNumber(input: String): Int? {
    return try {
        input.toInt()
    } catch (e: NumberFormatException) {
        println("Ошибка парсинга: ${e.message}")
        null // возвращаем null при ошибке
    } finally {
        println("Блок finally выполняется всегда")
    }
}

fun main() {
    val result = parseNumber("123abc")
    println(result) // null
}

Ключевые особенности:

  • try может быть использован как выражение — результат последнего выражения в блоке try или catch становится результатом всего блока.
  • Блок finally выполняется в любом случае (даже если было исключение или return).
  • В Kotlin нет проверяемых исключений (checked exceptions), как в Java — это упрощает код.

Важно помнить, что злоупотребление try/catch делает код менее читаемым. Используйте его только для действительно исключительных ситуаций, а не для управления потоком.

2. Тип Result и функциональный подход

Для более элегантной обработки ошибок Kotlin предлагает тип Result<T>. Это sealed class, который может быть либо Success (с результатом), либо Failure (с исключением). Он идеально подходит для функционального стиля.

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)
    
    // Обработка через when
    when (result) {
        is Result.Success -> println("Результат: ${result.value}")
        is Result.Failure -> println("Ошибка: ${result.exception.message}")
    }
    
    // Или через методы
    result.onSuccess { value -> println("Успех: $value") }
          .onFailure { e -> println("Провал: ${e.message}") }
    
    // Преобразование
    val mapped = result.map { it * 2 } // если failure, то останется failure
    println(mapped)
}

Преимущества Result:

  • Явно показывает, что функция может завершиться ошибкой.
  • Позволяет цепочки вызовов (map, flatMap, recover).
  • Не требует try/catch в вызывающем коде.

Однако Result не предназначен для хранения в коллекциях или передачи через границы корутин — для этого лучше использовать sealed class.

3. Sealed классы для детальной обработки ошибок

Когда нужно обрабатывать разные типы ошибок по-разному, sealed class — идеальный выбор. Вы можете создать иерархию ошибок и обрабатывать их в when с исчерпывающим покрытием.

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 timeoutMs: Long) : NetworkResult<Nothing>()
    data class UnknownError(val exception: Throwable) : NetworkResult<Nothing>()
}

fun fetchUser(id: Int): NetworkResult<String> {
    return when {
        id <= 0 -> NetworkResult.NetworkError(400, "Invalid ID")
        id == 1 -> NetworkResult.Timeout(5000)
        else -> NetworkResult.Success("User $id")
    }
}

fun main() {
    val result = fetchUser(-1)
    
    when (result) {
        is NetworkResult.Success -> println("Данные: ${result.data}")
        is NetworkResult.NetworkError -> println("HTTP ${result.code}: ${result.message}")
        is NetworkResult.Timeout -> println("Таймаут ${result.timeoutMs} мс")
        is NetworkResult.UnknownError -> println("Неизвестная ошибка: ${result.exception.message}")
    }
    // Компилятор не требует else, так как все ветки покрыты
}

Почему это круто?

  • Компилятор проверяет, что вы обработали все возможные случаи.
  • Легко добавлять новые типы ошибок без риска забыть их обработать.
  • Код становится самодокументируемым.

4. Продвинутые техники: Nothing, require, check и Elvis-оператор

Kotlin предоставляет дополнительные инструменты для управления ошибками без явного try/catch.

Тип Nothing — это специальный тип, который никогда не существует. Он используется для функций, которые всегда выбрасывают исключение или бесконечно зацикливаются.

fun fail(message: String): Nothing {
    throw IllegalArgumentException(message)
}

fun process(input: String): String {
    return if (input.isNotEmpty()) {
        input.uppercase()
    } else {
        fail("Input must not be empty") // компилятор знает, что это Nothing
    }
}

Функции require() и check():

fun setAge(age: Int) {
    require(age >= 0) { "Возраст не может быть отрицательным: $age" }
    require(age <= 150) { "Нереалистичный возраст: $age" }
    println("Возраст установлен: $age")
}

fun saveToDatabase(data: String) {
    check(data.isNotEmpty()) { "Данные не могут быть пустыми" }
    // ... сохранение
}