Обработка ошибок в 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()) { "Данные не могут быть пустыми" }
// ... сохранение
}
require()— для проверки аргументов (выбрасываетIllegalArgumentException).Блоги
Рекомендаций по книгам
Читайте также
Swift в 2025: зачем разработчику готовиться к революции уже сейчас
Обработка ошибок в JavaScript: try/catch, async/await и продвинутые паттерны
CI/CD для Go проектов: GitHub Actions от А до Я
Первый символ строки в Rust: разбираем популярный вопрос
Зарплаты JavaScript разработчиков в России: обзор рынка для продвинутых в 2024