Обработка ошибок в Kotlin: try/catch и продвинутые техники

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Обработка ошибок в Kotlin: try/catch и не только

Обработка ошибок — неотъемлемая часть написания надёжного и устойчивого кода. В Kotlin, как и в Java, существует классический механизм try/catch/finally, однако язык предлагает и более современные, функциональные подходы. В этой статье мы подробно разберём все способы обработки исключений в Kotlin: от базового try/catch до элегантного runCatching и мощных Result и sealed class.

1. Классический try/catch/finally

Базовый синтаксис обработки исключений в Kotlin практически идентичен Java, но с одним важным отличием: try может быть выражением, то есть возвращать значение.

Пример 1: try как выражение

fun parseIntOrNull(input: String): Int? {
    return try {
        input.toInt()
    } catch (e: NumberFormatException) {
        null
    }
}

fun main() {
    val result = parseIntOrNull("123")
    println(result) // 123

    val badResult = parseIntOrNull("abc")
    println(badResult) // null
}

Пример 2: Множественные блоки catch

fun processInput(input: String) {
    try {
        val number = input.toInt()
        val result = 100 / number
        println("Результат: $result")
    } catch (e: NumberFormatException) {
        println("Ошибка: неверный формат числа")
    } catch (e: ArithmeticException) {
        println("Ошибка: деление на ноль")
    } finally {
        println("Блок finally выполняется всегда")
    }
}

Блок finally выполняется в любом случае — как при успешном выполнении, так и при возникновении исключения. Это удобно для освобождения ресурсов (закрытие файлов, соединений).

2. Элвис-оператор (?:) и безопасные вызовы

Kotlin предлагает элегантный способ обработки null значений, который часто помогает избежать громоздких конструкций try/catch.

Пример: Безопасное преобразование с элвис-оператором

fun safeStringToInt(input: String): Int {
    return input.toIntOrNull() ?: 0 // Если null, возвращаем 0
}

fun main() {
    println(safeStringToInt("42"))   // 42
    println(safeStringToInt("abc"))  // 0
}

Функция toIntOrNull() — это встроенная в Kotlin альтернатива try/catch. Она не выбрасывает исключение, а возвращает null при ошибке. Элвис-оператор ?: подставляет значение по умолчанию.

3. Функциональный подход: runCatching и Result

Современный Kotlin рекомендует использовать функциональный стиль обработки ошибок. Ключевые инструменты — runCatching и класс Result.

Пример: runCatching

import kotlin.Result

fun divide(a: Int, b: Int): Result<Int> {
    return runCatching {
        a / b
    }
}

fun main() {
    val successResult = divide(10, 2)
    val failureResult = divide(10, 0)

    // Обработка результата
    successResult
        .onSuccess { println("Успех: $it") } // Успех: 5
        .onFailure { println("Ошибка: ${it.message}") }

    failureResult
        .onSuccess { println("Успех: $it") }
        .onFailure { println("Ошибка: ${it.message}") } // Ошибка: / by zero

    // Получение значения с дефолтом
    val value1 = successResult.getOrDefault(-1) // 5
    val value2 = failureResult.getOrDefault(-1) // -1

    // Преобразование результата
    val mappedResult = successResult.map { it * 2 } // Result(10)
}

Преимущества runCatching:

  • Код становится декларативным и читаемым.
  • Нет необходимости вручную писать try/catch.
  • Результат можно легко комбинировать, трансформировать и обрабатывать цепочками вызовов (.map, .flatMap, .recover).
  • Ошибка не «проглатывается» — она явно представлена в типе Result.

Важно: Класс Result в Kotlin — это value class, предназначенный для кратковременного использования. Не рекомендуется хранить его в полях классов или коллекциях.

4. Продвинутые техники: Sealed классы для моделирования ошибок

Для сложных доменов, где нужно различать типы ошибок, лучше всего подходят sealed class (запечатанные классы). Они позволяют создать иерархию ошибок и обрабатывать их в when-выражении с гарантией полноты покрытия.

Пример: Моделирование операций с файлом

import java.io.File
import java.io.IOException

// Моделируем возможные результаты операции
sealed class FileOperationResult {
    data class Success(val content: String) : FileOperationResult()
    data class NotFound(val path: String) : FileOperationResult()
    data class PermissionDenied(val path: String) : FileOperationResult()
    data class UnknownError(val exception: Throwable) : FileOperationResult()
}

fun readFileContent(path: String): FileOperationResult {
    return try {
        val file = File(path)
        if (!file.exists()) {
            return FileOperationResult.NotFound(path)
        }
        if (!file.canRead()) {
            return FileOperationResult.PermissionDenied(path)
        }
        FileOperationResult.Success(file.readText())
    } catch (e: IOException) {
        FileOperationResult.UnknownError(e)
    }
}

fun processFile(path: String) {
    when (val result = readFileContent(path)) {
        is FileOperationResult.Success -> {
            println("Файл прочитан. Длина: ${result.content.length}")
        }
        is FileOperationResult.NotFound -> {
            println("Ошибка: файл '$path' не найден.")
        }
        is FileOperationResult.PermissionDenied -> {
            println("Ошибка: нет прав на чтение файла '$path'.")
        }
        is FileOperationResult.UnknownError -> {
            println("Неизвестная ошибка: ${result.exception.message}")
        }
    }
}

Почему это круто?

  • Безопасность типов: Компилятор проверяет, что вы обработали все возможные варианты в when. Если вы добавите новый подкласс FileOperationResult

Блоги

Рекомендаций по книгам