Введение: почему эта тема взрывает Stack Overflow?
Если вы когда-нибудь писали на Kotlin и сталкивались с необходимостью перехватить несколько разных исключений, вы знаете, что это может быть больно. Особенно если вы пришли из Java, где мульти-кэтч (catch (IOException | SQLException e)) — это стандарт с Java 7. В Kotlin всё иначе, и многие разработчики теряются.
Вопрос "How to catch many exceptions at the same time in Kotlin?" на Stack Overflow собрал тысячи просмотров. И это не случайно: обработка ошибок — это то, с чем сталкиваются абсолютно все. Сегодня мы разберём эту задачу раз и навсегда.
В этой статье вы узнаете:
- Почему мульти-кэтч в Kotlin работает не как в Java
- Как красиво и компактно перехватывать несколько исключений
- Когда использовать
whenвcatch, а когда — старый добрыйif - Реальные примеры из боевого кода
Поехали!
Проблема: где мой мульти-кэтч?
В Java вы пишете просто:
try {
riskyOperation()
} catch (IOException | SQLException e) {
// обрабатываем одинаково
}
Красиво, лаконично, понятно. Но в Kotlin такого синтаксиса нет. Если вы попробуете написать catch (IOException | SQLException e), компилятор скажет вам «нет».
Почему? Потому что Kotlin сделал выбор в пользу явности и чистоты. Разработчики языка решили, что мульти-кэтч с вертикальной чертой — это не совсем стиль Kotlin. Но это не значит, что вы не можете поймать несколько исключений. Просто делается это иначе.
Давайте посмотрим на три основных способа.
Способ 1: множественные блоки catch (самый очевидный)
Самый простой путь — просто добавить несколько блоков catch подряд. Kotlin это поддерживает на ура.
try {
val data = loadData()
processData(data)
} catch (e: IOException) {
println("Ошибка ввода-вывода: ${e.message}")
} catch (e: SQLException) {
println("Ошибка базы данных: ${e.message}")
} catch (e: Exception) {
println("Неизвестная ошибка: ${e.message}")
}
Плюсы:
- Предельно ясно — каждый тип исключения обрабатывается отдельно
- Можно дать разную логику для разных ошибок
- Отлично читается новичками
Минусы:
- Много дублирования кода, если логика обработки одинаковая
- При 5+ исключениях блок становится громоздким
Способ 2: catch с when (элегантный и гибкий)
Вот где Kotlin раскрывается во всей красе. Вместо мульти-кэтч вы можете использовать when внутри одного блока catch.
try {
val result = fetchFromNetwork()
saveToDatabase(result)
} catch (e: Exception) {
when (e) {
is IOException -> handleNetworkError(e)
is SQLException -> handleDatabaseError(e)
is SecurityException -> logAndNotify(e)
else -> throw e // перебрасываем, если не знаем как обработать
}
}
Этот подход — настоящая находка. Он позволяет:
- Обрабатывать разные исключения в одном месте
- Использовать все возможности
when, включая проверки условий - Легко добавлять новые типы исключений
Вот более продвинутый пример с условиями:
try {
connectToService()
} catch (e: Exception) {
when {
e is IOException && e.message?.contains("timeout") == true ->
println("Таймаут соединения, пробуем снова")
e is IOException ->
println("Ошибка сети: ${e.message}")
e is SQLException && e.errorCode == 1045 ->
println("Ошибка аутентификации в БД")
else ->
throw RuntimeException("Неизвестная ошибка", e)
}
}
Когда использовать? Когда у вас 3+ исключения, которые требуют разной, но связанной логики.
Способ 3: sealed классы и пользовательские исключения (для профи)
Если вы проектируете свою архитектуру с нуля, рассмотрите sealed классы. Они позволяют создать иерархию исключений и обрабатывать их на новом уровне.
sealed class AppException(message: String, cause: Throwable? = null) : Exception(message, cause) {
class NetworkException(message: String, cause: Throwable? = null) : AppException(message, cause)
class DatabaseException(message: String, cause: Throwable? = null) : AppException(message, cause)
class AuthenticationException(message: String, cause: Throwable? = null) : AppException(message, cause)
}
// Использование
fun process() {
try {
riskyOperation()
} catch (e: AppException) {
when (e) {
is AppException.NetworkException -> handleNetwork(e)
is AppException.DatabaseException -> handleDatabase(e)
is AppException.AuthenticationException -> handleAuth(e)
}
}
}
Это даёт вам:
- Полный контроль над типами ошибок
- Компилятор проверяет, что вы обработали все варианты (если используете
whenкак выражение) - Чистую архитектуру без сюрпризов
А как же Kotlin-стиль? Советы от сообщества
На Stack Overflow часто встречается вопрос: «Что считается best practice?». Вот консенсус сообщества:
- Не ловите Exception везде. Это антипаттерн. Ловите только те исключения, которые можете обработать осмысленно.
- Используйте Result. В Kotlin есть
Result<T>, который помогает избежать try-catch вообще:
fun loadData(): Result<String> = runCatching {
// код, который может выбросить исключение
fetchFromApi()
}
// Использование
val result = loadData()
result.onSuccess { data -> println("Данные: $data") }
.onFailure { error -> println("Ошибка: ${error.message}") }
3. Избегайте пустых catch. Никогда не пишите:
catch (e: Exception) {}
Это скрывает ошибки и делает дебаг адом.
Пример из реального проекта: загрузка данных из сети и БД
Давайте соберём всё вместе. Представьте, что у вас есть функция, которая загружает данные из сети, кэширует их в БД и возвращает пользователю. Здесь могут возникнуть разные ошибки: сетевые проблемы, сбой записи в БД, неверный формат данных.
fun loadAndCacheData(): String {
return try {
val networkData = fetchFromNetwork() // может быть IOException, SocketTimeoutException