--- title: Система исключений description: Иерархия Throwable, try как выражение, отсутствие checked exceptions и правильная обработка tags: - kotlin - exceptions --- Все исключения в Kotlin **непроверяемые**. Компилятор не требует ни объявлять, ни перехватывать их. ```kotlin fun parse(s: String): Int = s.toInt() // может бросить, но throws не нужен fun main() { println(parse("42")) // 42 try { parse("abc") } catch (e: NumberFormatException) { println("не число: ${e.message}") // не число: For input string: "abc" } } ``` ## Иерархия ```mermaid graph TD T["Throwable"] --> E["Exception"] T --> R["Error"] E --> RE["RuntimeException"] E --> IOE["IOException"] RE --> NPE["NullPointerException"] RE --> IAE["IllegalArgumentException"] RE --> ISE["IllegalStateException"] RE --> CCE["ClassCastException"] R --> OOM["OutOfMemoryError"] R --> SOE["StackOverflowError"] ``` Иерархия та же, что в Java, - это классы JDK. Разница только в том, что Kotlin не делит их на проверяемые и непроверяемые. | Класс | Смысл | Перехватывать | |---|---|---| | `Error` | сбой среды: нет памяти, переполнен стек | нет | | `RuntimeException` | ошибка программиста: неверный аргумент, состояние | обычно нет, лучше исправить код | | прочие `Exception` | внешний сбой: сеть, файл, БД | да | ## try как выражение `try` возвращает значение - это важное отличие от Java: ```kotlin fun parseOrZero(s: String): Int = try { s.toInt() } catch (e: NumberFormatException) { 0 } fun main() { println(parseOrZero("42")) // 42 println(parseOrZero("abc")) // 0 } ``` Результат - последнее выражение блока `try` или соответствующего `catch`. Блок `finally` на результат **не влияет**: ```kotlin fun main() { val result = try { "из try" } finally { "из finally" // игнорируется } println(result) // из try } ``` ## throw - тоже выражение Тип `throw` - [Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md), поэтому он подходит в любую позицию: ```kotlin fun findUser(id: Int): String = if (id > 0) "пользователь $id" else throw IllegalArgumentException("неверный id: $id") fun main() { println(findUser(1)) // пользователь 1 val name = findUser(1).takeIf { it.isNotEmpty() } ?: throw IllegalStateException("пусто") println(name) // пользователь 1 } ``` Это позволяет писать `?: throw ...` и `?: error(...)` в одном выражении. ## Перехват ```kotlin fun risky(mode: Int): String = when (mode) { 1 -> throw IllegalArgumentException("плохой аргумент") 2 -> throw IllegalStateException("плохое состояние") else -> "ок" } fun handle(mode: Int): String = try { risky(mode) } catch (e: IllegalArgumentException) { "аргумент: ${e.message}" } catch (e: IllegalStateException) { "состояние: ${e.message}" } catch (e: Exception) { "прочее: ${e.message}" } finally { println("finally выполняется всегда") } fun main() { println(handle(1)) // аргумент: плохой аргумент println(handle(3)) // ок } ``` Блоки `catch` проверяются по порядку, поэтому конкретные типы идут раньше общих. `catch (e: Exception)` первым сделал бы остальные недостижимыми. Несколько типов в одном блоке Kotlin не поддерживает - аналога Java `catch (A | B e)` нет. Варианты: ```kotlin fun main() { try { throw IllegalStateException("сбой") } catch (e: RuntimeException) { // общий супертип println("перехвачено: ${e::class.simpleName}") } try { throw NumberFormatException("плохое число") } catch (e: Exception) { when (e) { // или when по типу is NumberFormatException, is IllegalStateException -> println("ожидаемая ошибка: ${e.message}") else -> throw e // остальное пробрасываем } } } ``` ## Свои исключения ```kotlin class InsufficientFundsException( val required: Long, val available: Long, ) : Exception("нужно $required, доступно $available") class ValidationException(message: String, cause: Throwable? = null) : Exception(message, cause) fun withdraw(balance: Long, amount: Long): Long { if (amount > balance) throw InsufficientFundsException(amount, balance) return balance - amount } fun main() { println(withdraw(100, 30)) // 70 try { withdraw(100, 500) } catch (e: InsufficientFundsException) { println("не хватает ${e.required - e.available}") // не хватает 400 } } ``` Своё исключение полезно, когда вызывающему нужно различить ситуацию и получить данные о ней. Для «ошибки программиста» хватает стандартных `IllegalArgumentException` и `IllegalStateException`. ## Цепочка причин ```kotlin class RepositoryException(message: String, cause: Throwable) : Exception(message, cause) fun loadUser(id: String): String = try { "пользователь ${id.toInt()}" } catch (e: NumberFormatException) { throw RepositoryException("не удалось загрузить '$id'", e) // сохраняем причину } fun main() { try { loadUser("abc") } catch (e: RepositoryException) { println(e.message) // не удалось загрузить 'abc' println("причина: ${e.cause?.message}") // причина: For input string: "abc" } } ``` Передавать `cause` обязательно: без него теряется исходный стектрейс и причина сбоя. ## use - автоматическое закрытие Аналог try-with-resources: ```kotlin import java.io.StringReader fun main() { val content = StringReader("данные").use { reader -> reader.readText() // close() вызовется автоматически } println(content) // данные } ``` `use` - функция-расширение для `Closeable`, она закрывает ресурс и при исключении. Это предпочтительный способ: `finally` с ручным `close()` легко написать неверно. ```kotlin class Resource(val name: String) : AutoCloseable { fun work() = "работаю с $name" override fun close() = println("$name закрыт") } fun main() { val result = Resource("файл").use { it.work() } println(result) // файл закрыт // работаю с файл try { Resource("другой").use { throw IllegalStateException("сбой") } } catch (e: IllegalStateException) { println("ресурс всё равно закрыт: ${e.message}") } } ``` ## @Throws для Java Kotlin не объявляет исключения, но Java-клиенту это может быть нужно: ```kotlin import java.io.IOException @Throws(IOException::class) fun readFile(path: String): String { if (path.isEmpty()) throw IOException("пустой путь") return "содержимое $path" } fun main() { println(readFile("a.txt")) } // содержимое a.txt ``` Без `@Throws` Java-код не сможет перехватить проверяемое исключение - компилятор Java сочтёт, что метод его не бросает. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) ## Чего не делать ```kotlin fun main() { // проглатывание: ошибка исчезает try { error("сбой") } catch (e: Exception) { // ничего — худшая практика } // перехват Throwable: ловит OutOfMemoryError и StackOverflowError try { error("сбой") } catch (e: Exception) { // не Throwable println("перехвачено корректно") } // исключение как управление потоком val items = listOf(1, 2, 3) println(items.getOrNull(10) ?: "нет элемента") // вместо try/catch } ``` Правила: - не перехватывать, если не знаете, что делать с ошибкой - пусть идёт выше - не перехватывать `Throwable` и `Error` - не использовать исключения для ожидаемых ситуаций - для них есть `null`, `Result` или sealed-типы - всегда логировать или пробрасывать; пустой `catch` скрывает причину сбоя Альтернативы исключениям: [Result и runCatching](Result%20и%20runCatching.md) ## Исключения в корутинах Отмена корутины реализована через `CancellationException`, и перехватывать её нельзя: ```kotlin // Требует kotlinx-coroutines-core. // suspend fun work() { // try { // delay(1000) // } catch (e: CancellationException) { // throw e // обязательно пробросить! // } catch (e: Exception) { // println("настоящая ошибка") // } // } ``` `catch (e: Exception)` перехватит и отмену, из-за чего корутина продолжит работу после отмены. Подробнее: [Исключения в корутинах](../Корутины/Исключения%20в%20корутинах.md) > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | checked exceptions | **нет**, все непроверяемые | > | `throws IOException` в сигнатуре | `@Throws` только для Java-клиентов | > | `try` - инструкция | `try` - выражение | > | `catch (A \| B e)` | нет аналога, нужен `when` | > | try-with-resources | `use { }` | > | `throw` - инструкция | `throw` - выражение типа `Nothing` | > > **Отсутствие checked exceptions - самое спорное решение.** Аргументы против них, которые приводят авторы Kotlin: > > 1. **Они не масштабируются.** Сигнатура с пятью `throws` нечитаема, а обобщённый код (`map`, `forEach`) не может объявить исключения вызываемой лямбды - из-за этого Java-стримы несовместимы с проверяемыми исключениями. > 2. **На практике их проглатывают.** Самый частый способ «обработать» обязательное исключение - пустой `catch`, то есть защита превращается в сокрытие ошибки. > 3. **Обёртывание ради компиляции.** Код вроде `catch (IOException e) { throw new RuntimeException(e); }` встречается в любом крупном Java-проекте. > > Цена отказа - сигнатура больше не документирует ошибки. Это компенсируют KDoc и типы вроде `Result`, но компилятор уже не помогает. > > **`try` как выражение убирает `var`:** > ```java > int value; // Java: нужна изменяемая переменная > try { value = Integer.parseInt(s); } > catch (NumberFormatException e) { value = 0; } > ``` > ```kotlin > fun parseOrZero(s: String): Int = > try { s.toInt() } catch (e: NumberFormatException) { 0 } // val вместо var > > fun main() { println(parseOrZero("abc")) } // 0 > ``` > > **`use` вместо try-with-resources** - обычная функция, а не синтаксис. Поэтому её можно применить к своему типу, добавив реализацию `Closeable`. > > Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md) > [!warning] Подводные камни > **Сигнатура не говорит об исключениях.** Единственные источники информации - документация и чтение кода. Для важных случаев указывайте в KDoc: > ```kotlin > /** > * @throws IllegalArgumentException если id не положительный > */ > fun load(id: Int): String { > require(id > 0) { "id должен быть положительным" } > return "данные $id" > } > > fun main() { println(load(1)) } // данные 1 > ``` > > **`finally` не влияет на результат `try`-выражения** - значение из него отбрасывается. > > **`return` в `finally` проглатывает исключение:** > ```kotlin > fun bad(): String { > try { > throw IllegalStateException("потеряется") > } finally { > return "из finally" // исключение исчезло! > } > } > > fun main() { println(bad()) } // из finally > ``` > Исключение не дойдёт до вызывающего. `return` в `finally` писать нельзя. > > **Перехват `Exception` ловит все `RuntimeException`,** включая `NullPointerException` в вашем коде - то есть маскирует собственные баги под «обработанную ошибку». > > **`CancellationException` нельзя проглатывать в корутинах** - иначе отмена перестанет работать. > > **Исключение в `init` или в инициализаторе свойства оставляет побочные эффекты.** Объект не создастся, но то, что уже выполнилось, останется. Подробнее: [Инициализация](../ООП/Инициализация.md) > > **Создание исключения дорого** из-за сбора стектрейса. Для ожидаемых ситуаций в горячем коде используйте `null` или `Result`: > ```kotlin > fun main() { > println("abc".toIntOrNull() ?: 0) // без исключения > // println("abc".toInt()) // исключение + стектрейс > } > ``` > > **Исключение из лямбды в `forEach` прерывает весь цикл**, а не одну итерацию: > ```kotlin > fun main() { > val results = mutableListOf() > try { > listOf("1", "abc", "3").forEach { results.add(it.toInt()) } > } catch (e: NumberFormatException) { > println("прервано на второй строке, собрано: $results") // [1] > } > } > ``` > Чтобы обработать все элементы, перехватывайте внутри лямбды или используйте `mapNotNull { it.toIntOrNull() }`. > > **`@Throws` не проверяется компилятором Kotlin** - это только метаданные для Java. Можно объявить исключение, которое никогда не бросается, и наоборот. Далее: [Result и runCatching](Result%20и%20runCatching.md)