Исключения и обработка ошибок

В Kotlin все исключения непроверяемые, try-catch является выражением, а для функционального стиля есть runCatching и тип Result.
Суть: грамотная обработка ошибок отличает учебный код от продакшен-кода; Kotlin даёт для этого как классический try-catch, так и удобный функциональный Result.

В отличие от Java, в Kotlin нет проверяемых исключений: компилятор не заставляет объявлять, какие исключения может бросить функция. Это упрощает код, но возлагает ответственность за обработку на разработчика. Базовый инструмент — try-catch-finally, и, как многое в Kotlin, try — это выражение, возвращающее значение.

fun parseOrZero(text: String): Int {
    return try {
        text.toInt()        // может бросить NumberFormatException
    } catch (e: NumberFormatException) {
        0                   // значение по умолчанию при ошибке
    }
}

println(parseOrZero("42"))   // 42
println(parseOrZero("abc"))  // 0

Бросаем свои исключения

Бросить исключение можно через throw. Часто используют IllegalArgumentException для неверных аргументов. Есть удобные функции-проверки: require бросает исключение, если условие ложно, а check — для проверки состояния.

fun withdraw(balance: Int, amount: Int): Int {
    require(amount > 0) { "Сумма должна быть положительной" }
    require(amount <= balance) { "Недостаточно средств" }
    return balance - amount
}

println(withdraw(100, 30))  // 70
// withdraw(100, 200)       // IllegalArgumentException: Недостаточно средств

Как работает под капотом

Для функционального стиля Kotlin предлагает runCatching: он выполняет блок и оборачивает результат в тип Result — либо успех со значением, либо неудачу с исключением. Это удобно, когда ошибку нужно передать дальше как данные, а не как прерывание потока. На Result навешивают onSuccess, onFailure, getOrElse, getOrNull.

val result = runCatching { "123".toInt() }
result
    .onSuccess { println("Успех: $it") }
    .onFailure { println("Ошибка: ${it.message}") }

val value = runCatching { "x".toInt() }.getOrElse { -1 }
println(value)  // -1
  runCatching { ... }  -->  Result
                              |
            +-----------------+-----------------+
            v                                   v
        Success(value)                     Failure(exception)
            |                                   |
        getOrNull() = value             getOrNull() = null

Частые ошибки

Глотать исключения молча. Пустой catch {} прячет проблему, и баг проявится позже в непонятном месте. Минимум — залогируйте ошибку.

Ловить слишком общий тип. catch (e: Exception) перехватывает всё подряд, включая то, что вы не ожидали. Ловите конкретные типы.

Использовать исключения для управления потоком. Исключения — для исключительных ситуаций, а не для обычных ветвлений; для ожидаемых случаев лучше nullable или Result.

Best practices

  • Ловите конкретные типы исключений, а не общий Exception.
  • Используйте require/check для проверки аргументов и состояния.
  • Для ошибок как данных применяйте runCatching и Result.
  • Никогда не оставляйте catch пустым — как минимум логируйте.

Логику безопасного парсинга с запасным значением легко повторить на Python. Запустите врезку.

# Аналог try-catch с значением по умолчанию из Kotlin
def parse_or_zero(text):
    try:
        return int(text)
    except ValueError:
        return 0

for s in ['42', 'abc', '-7', '3.14']:
    print(repr(s), '->', parse_or_zero(s))

Попробуй сам ▶ — добавьте обработку дробных чисел или своё значение по умолчанию. В Kotlin это был бы try { text.toInt() } catch (...) { ... }.

Закрепим главное

Держите в голове границу между ожидаемым и исключительным. Если отсутствие значения — нормальный сценарий (пользователь не найден, поле не заполнено), выражайте это через nullable-тип или Result, а не через брошенное исключение. Исключения дороги и сбивают линейность кода, поэтому их место — действительно нештатные ситуации: оборванная сеть, повреждённые данные, нарушенный инвариант. Эта дисциплина делает поток управления честным: по сигнатуре функции видно, что она может не вернуть значение.

Второй ориентир пригодится в Android напрямую. Сетевой и файловый код падает регулярно — это норма, а не редкость. Поэтому ещё до написания UI продумывайте, как ошибка превратится в состояние экрана: вспомните sealed UiState с веткой Error. Связка «поймали исключение в слое данных — превратили в состояние ошибки — показали пользователю понятное сообщение» отличает приложение, которое переживает плохую сеть, от приложения, которое на ней падает.

Итог: Kotlin даёт гибкую модель ошибок — от классического try-catch-выражения до функционального Result. Грамотная обработка ошибок особенно важна в сетевом коде Android, к которому мы придём в последнем разделе.

Проверьте себя
1. Чем исключения в Kotlin отличаются от Java?
AВ Kotlin исключений нет вообще
BВ Kotlin все исключения непроверяемые: компилятор не требует их объявлять или обрабатывать
CВ Kotlin исключения нельзя ловить
DВ Kotlin исключения работают медленнее
2. Что возвращает функция runCatching?
AВсегда null при ошибке
BТип Result, который содержит либо успешное значение, либо пойманное исключение
CТекст ошибки строкой
DЛогическое значение true/false