16 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Система исключений | Иерархия Throwable, try как выражение, отсутствие checked exceptions и правильная обработка |
|
Все исключения в 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"
}
}
Иерархия
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:
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 на результат не влияет:
fun main() {
val result = try {
"из try"
} finally {
"из finally" // игнорируется
}
println(result) // из try
}
throw - тоже выражение
Тип throw - Nothing, поэтому он подходит в любую позицию:
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(...) в одном выражении.
Перехват
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) нет. Варианты:
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 // остальное пробрасываем
}
}
}
Свои исключения
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.
Цепочка причин
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:
import java.io.StringReader
fun main() {
val content = StringReader("данные").use { reader ->
reader.readText() // close() вызовется автоматически
}
println(content) // данные
}
use - функция-расширение для Closeable, она закрывает ресурс и при исключении. Это предпочтительный способ: finally с ручным close() легко написать неверно.
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-клиенту это может быть нужно:
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
Чего не делать
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
Исключения в корутинах
Отмена корутины реализована через CancellationException, и перехватывать её нельзя:
// Требует kotlinx-coroutines-core.
// suspend fun work() {
// try {
// delay(1000)
// } catch (e: CancellationException) {
// throw e // обязательно пробросить!
// } catch (e: Exception) {
// println("настоящая ошибка")
// }
// }
catch (e: Exception) перехватит и отмену, из-за чего корутина продолжит работу после отмены. Подробнее: Исключения в корутинах
[!info] Отличия от Java
Java Kotlin checked exceptions нет, все непроверяемые throws IOExceptionв сигнатуре@Throwsтолько для Java-клиентовtry- инструкцияtry- выражениеcatch (A | B e)нет аналога, нужен whentry-with-resources use { }throw- инструкцияthrow- выражение типаNothingОтсутствие checked exceptions - самое спорное решение. Аргументы против них, которые приводят авторы Kotlin:
- Они не масштабируются. Сигнатура с пятью
throwsнечитаема, а обобщённый код (map,forEach) не может объявить исключения вызываемой лямбды - из-за этого Java-стримы несовместимы с проверяемыми исключениями.- На практике их проглатывают. Самый частый способ «обработать» обязательное исключение - пустой
catch, то есть защита превращается в сокрытие ошибки.- Обёртывание ради компиляции. Код вроде
catch (IOException e) { throw new RuntimeException(e); }встречается в любом крупном Java-проекте.Цена отказа - сигнатура больше не документирует ошибки. Это компенсируют KDoc и типы вроде
Result, но компилятор уже не помогает.
tryкак выражение убираетvar:int value; // Java: нужна изменяемая переменная try { value = Integer.parseInt(s); } catch (NumberFormatException e) { value = 0; }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
[!warning] Подводные камни Сигнатура не говорит об исключениях. Единственные источники информации - документация и чтение кода. Для важных случаев указывайте в KDoc:
/** * @throws IllegalArgumentException если id не положительный */ fun load(id: Int): String { require(id > 0) { "id должен быть положительным" } return "данные $id" } fun main() { println(load(1)) } // данные 1
finallyне влияет на результатtry-выражения - значение из него отбрасывается.
returnвfinallyпроглатывает исключение:fun bad(): String { try { throw IllegalStateException("потеряется") } finally { return "из finally" // исключение исчезло! } } fun main() { println(bad()) } // из finallyИсключение не дойдёт до вызывающего.
returnвfinallyписать нельзя.Перехват
Exceptionловит всеRuntimeException, включаяNullPointerExceptionв вашем коде - то есть маскирует собственные баги под «обработанную ошибку».
CancellationExceptionнельзя проглатывать в корутинах - иначе отмена перестанет работать.Исключение в
initили в инициализаторе свойства оставляет побочные эффекты. Объект не создастся, но то, что уже выполнилось, останется. Подробнее: ИнициализацияСоздание исключения дорого из-за сбора стектрейса. Для ожидаемых ситуаций в горячем коде используйте
nullилиResult:fun main() { println("abc".toIntOrNull() ?: 0) // без исключения // println("abc".toInt()) // исключение + стектрейс }Исключение из лямбды в
forEachпрерывает весь цикл, а не одну итерацию:fun main() { val results = mutableListOf<Int>() try { listOf("1", "abc", "3").forEach { results.add(it.toInt()) } } catch (e: NumberFormatException) { println("прервано на второй строке, собрано: $results") // [1] } }Чтобы обработать все элементы, перехватывайте внутри лямбды или используйте
mapNotNull { it.toIntOrNull() }.
@Throwsне проверяется компилятором Kotlin - это только метаданные для Java. Можно объявить исключение, которое никогда не бросается, и наоборот.
Далее: Result и runCatching