Error Propagation: что это, механизм распространения ошибок и как работает в мобильной разработке

Автор: IT Sectr Опубликовано: 2026-05-26 Время чтения: 9 мин

Error Propagation — механизм распространения ошибки вверх по стеку вызовов от места возникновения до обработчика. Когда функция не может обработать ошибку самостоятельно, она передаёт её вызывающей стороне через исключение (exception), throws-декларацию или Return Type. Правильная реализация propagation критична для стабильности мобильных приложений: необработанные или некорректно переданные ошибки приводят к крашам. По данным Apple Swift Documentation (2026), автоматическая propagation через throws в Swift позволяет передавать ошибку на любой уровень без boilerplate-кода.

Главное

  • Error Propagation — передача ошибки от места возникновения вверх по стеку к обработчику, минуя промежуточные функции
  • Автоматическая propagation через throws в Swift передаёт ошибку без явного кода на каждом уровне стека
  • Ручная propagation в Kotlin и Dart требует явного try-catch или передачи в Result-контейнере на каждом уровне
  • Checked exceptions в Java принуждают к propagation через throws в сигнатуре, unchecked — позволяют игнорировать
  • Result Type — альтернатива исключениям, где ошибка передаётся как значение без раскрутки стека

Что такое Error Propagation?

Error Propagation (распространение ошибки) — это процесс передачи объекта ошибки от функции, где она возникла, вверх по цепочке вызовов до ближайшего подходящего обработчика. Представьте стек вызовов: ViewController вызывает ViewModel, ViewModel вызывает Repository, Repository вызывает API. Если API возвращает ошибку сети, она должна пройти через Repository и ViewModel к ViewController, который покажет сообщение пользователю. Каждая промежуточная функция решает: обработать ошибку или передать дальше (propagate).

Существует два подхода к propagation: автоматический и ручной. При автоматическом подходе (Swift throws, Java checked exceptions) компилятор принуждает разработчика либо обработать ошибку, либо декларировать propagation в сигнатуре. При ручном подходе (Result Type, Kotlin Try) ошибка передаётся как значение — разработчик явно пишет код для передачи или трансформации ошибки. По данным Kotlin Result Docs (2026), Result<T> в Kotlin не предназначен для propagation через границы функций напрямую — его нужно трансформировать или обрабатывать на каждом уровне, что делает propagation более осознанным, но и более многословным.

Выбор подхода зависит от архитектуры приложения и языка. В Swift доминирует автоматическая propagation через throws, в Kotlin — микс исключений (для неожиданных ошибок) и Result-подобных контейнеров (для ожидаемых). Важно понимать: propagation — это не цель, а необходимость. Идеальная архитектура минимизирует глубину propagation, обрабатывая ошибки на максимально низком уровне, где есть достаточно контекста для принятия решения.

Propagation через Throws в Swift

В Swift propagation через throws происходит автоматически: если функция A с throws вызывает функцию B с throws, и A не обрабатывает ошибку B в do-catch, ошибка автоматически передаётся вызывающей стороне A. Это избавляет от boilerplate-кода, характерного для Java checked exceptions, где throws нужно декларировать в каждом методе цепочки. Swift использует принцип «одна throws-функция в цепочке = всё цепочка становится throws, если не обрабатывать на промежуточных уровнях».

swift
struct UserRepository {
    func fetchUser(id: Int) throws -> User {
        let data = try networkService.request(path: "/users/\(id)")
        return try parseUser(from: data)
    }
}

class UserViewModel {
    let repo = UserRepository()

    func loadUser(id: Int) throws -> User {
        return try repo.fetchUser(id: id)
    }
}

// ViewController — конечный обработчик
func onButtonTap() {
    let vm = UserViewModel()
    do {
        let user = try vm.loadUser(id: 42)
        updateUI(user)
    } catch {
        showError("Failed to load user")
    }
}

Цепочка propagation: networkService.request -> fetchUser -> loadUser -> onButtonTap. Каждая промежуточная функция помечена throws и не содержит do-catch — ошибка автоматически передаётся наверх. ViewController onButtonTap — конечный обработчик с do-catch. Если бы ViewModel решила трансформировать ошибку (завернуть в другой тип), она могла бы использовать do-catch и новый throw. Автоматическая propagation сокращает код: Repository не нужно знать, как обрабатывать ошибку — это ответственность ViewController, который имеет доступ к UI для показа сообщения пользователю.

Propagation через исключения в Kotlin

В Kotlin propagation через исключения не требует декларации throws в сигнатуре (все исключения unchecked). Исключение автоматически поднимается по стеку, пока не встретит try-catch. Однако отсутствие throws в сигнатуре делает propagation неявной: разработчик не видит из сигнатуры функции, что она может выбросить исключение. Это плюс (меньше boilerplate) и минус (легче забыть обработать) одновременно. Kotlin решает эту проблему через соглашения и архитектурные паттерны, а не через язык.

kotlin
class UserRepository(
    private val api: ApiService,
    private val db: Database
) {
    suspend fun getUser(id: String): User {
        return try {
            api.fetchUser(id)
        } catch (e: IOException) {
            db.getCachedUser(id) ?: throw AppException("User unavailable")
        }
    }
}

class UserViewModel(private val repo: UserRepository) {
    private val _state = MutableStateFlow<UiState<User>>(UiState.Loading)
    val state: StateFlow<UiState<User>> = _state

    fun loadUser(id: String) {
        viewModelScope.launch {
            try {
                val user = repo.getUser(id)
                _state.value = UiState.Success(user)
            } catch (e: AppException) {
                _state.value = UiState.Error(e.message ?: "Unknown")
            }
        }
    }
}

В Repository propagation с трансформацией: при IOException (сеть недоступна) функция пытается получить закэшированные данные из БД. Если кэш пуст, выбрасывает AppException — propagation продолжается уже с новым типом ошибки. ViewModel перехватывает AppException и транслирует в UiState.Error — ошибка не идёт дальше, propagation завершена на уровне UI-слоя. Kotlin Coroutines добавляют особенности: исключения в launch автоматически распространяются через CoroutineExceptionHandler, а в async — только при вызове await(). Это важно учитывать при проектировании propagation корутинах — SupervisorJob предотвращает отмену родительской корутины при ошибке в дочерней.

Propagation через Result Type

Альтернатива исключениям — propagation через тип-контейнер, который передаёт успех или ошибку как значение. В этом подходе функция возвращает не значение, а обёртку: Result<T, E> в Swift, Result<T> в Kotlin, Either<L, R> в Dart (из пакета fpdart или dartz). Ошибка не раскручивает стек — она просто лежит в контейнере, и следующий уровень решает, что с ней делать. Это делает propagation более явной и контролируемой.

kotlin
data class HttpResult<out T>(
    val data: T?,
    val error: AppError?
) {
    val isSuccess: Boolean get() = data != null
    val isError: Boolean get() = error != null
}

sealed class AppError {
    data class Network(val message: String) : AppError()
    data class Auth(val message: String) : AppError()
}

fun fetchUser(id: String): HttpResult<User> {
    return try {
        val response = api.get("/users/$id")
        HttpResult(data = parseUser(response), error = null)
    } catch (e: IOException) {
        HttpResult(data = null, error = AppError.Network("No internet"))
    }
}

HttpResult<T> — простой контейнер с полями data и error. Sealed class AppError определяет типы ошибок (Network, Auth). Функция fetchUser возвращает HttpResult, propagation не требует раскрутки стека — вызывающая сторона просто проверяет isSuccess/isError. Такой подход особенно полезен в Clean Architecture, где каждый слой (data, domain, presentation) может трансформировать ошибку: IOError -> DomainError -> UiError. Propagation через контейнер делает эти трансформации явными и тестируемыми, в отличие от исключений, где цепочка преобразований не видна в сигнатурах функций.

Propagation vs Handling: когда передавать, когда обрабатывать

Одно из ключевых решений в проектировании обработки ошибок — выбор между propagation (передать выше) и handling (обработать здесь). Правило принятия решения: обрабатывай ошибку на том уровне, где есть достаточно контекста для meaningful действия. Если у тебя есть доступ к UI — покажи сообщение пользователю. Если есть доступ к кэшу — попробуй восстановиться. Если нет ни того, ни другого — propagate.

СценарийДействиеОбоснование
Ошибка сети в RepositoryPropagateRepository не знает, хочет ли пользователь повторить запрос
Ошибка парсинга в RepositoryОбработать (вернуть default)Repository знает формат, может вернуть fallback-значение
Таймаут в ViewModelОбработать (UiState.Error)ViewModel управляет UiState, знает, как транслировать ошибку
Ошибка авторизации в InterceptorОбработать (refresh token)Interceptor имеет доступ к токенам и может восстановить сессию
Неизвестная ошибка в UseCasePropagateUseCase не имеет UI-контекста — только бизнес-логика

Золотое правило: минимум propagation, максимум handling на нижних уровнях. Если Repository может восстановиться из кэша — он должен это сделать, не передавая ошибку выше. Если ViewModel может показать Snackbar — пусть покажет, не требуя от ViewController дополнительного кода. Каждый уровень propagation увеличивает связанность и усложняет тестирование. По данным Google Android Architecture Guide (2026), рекомендуется минимизировать propagation через границы слоёв, используя sealed class UiState для представления всех возможных состояний (Loading, Success, Error) на уровне ViewModel, и не передавать исключения в UI-слой напрямую.

Проблемы и антипаттерны Error Propagation

Некорректная propagation — источник трудно отлавливаемых багов в мобильных приложениях. Рассмотрим пять основных проблем, с которыми сталкиваются разработчики, и способы их решения.

Потеря контекста ошибки

Самая частая проблема: при propagation исключение перехватывается, логируется и выбрасывается новое без original exception. Разработчик теряет StackTrace и не может понять, где именно возникла ошибка. В Swift используйте error chaining: throw MyError(context: originalError). В Kotlin: throw AppException(cause = originalException). В Dart: throw AppException(message, originalException). Никогда не создавайте новое исключение без передачи cause/underlyingError.

Игнорирование ошибки (пустой catch)

catch (e: Exception) { /* ничего */ } — антипаттерн, который приводит к тому, что приложение продолжает работу в некорректном состоянии. Если вы уверены, что ошибку можно игнорировать — добавьте комментарий с обоснованием. В Swift для опционального игнорирования используйте try? (ошибка -> nil). В Kotlin — Result<T>.onFailure { /* log */ }. Не глушите исключения без логирования.

Чрезмерная глубина propagation

Если ошибка проходит через 5+ уровней без обработки, архитектура требует пересмотра. Каждый уровень propagation — это зависимость от сигнатуры throws нижележащих функций. Решение: используйте Failure-контейнеры (sealed class Result { Success, Error }) на границах слоёв, чтобы сделать propagation явной и ограниченной. Чем меньше цепочка propagation, тем проще тестировать и отлаживать код.

Propagation в корутинах без SupervisorJob

В Kotlin Coroutines исключение в launch по умолчанию отменяет родительскую корутину и всех siblings (детей того же scope). Если одна задача из 10 параллельных упала, остальные 9 будут отменены, что часто нежелательно. Используйте SupervisorJob или supervisorScope для изоляции ошибок: ошибка в одном child не отменяет siblings. ViewModelScope использует SupervisorJob по умолчанию, что спасает от этой проблемы в Android.

Propagation через callback без обработки

В callback-based API ошибка часто передаётся как параметр колбэка. Если callback не обрабатывает ошибку (или обрабатывает некорректно), propagation становится неявной и легко теряется. Решение: мигрируйте на async/await (Swift) или корутины (Kotlin), где propagation работает через стандартные механизмы try-catch. Если callback неизбежен — используйте Either<Error, T> или Result<T> для принудительной обработки обоих кейсов.

Часто задаваемые вопросы

Чем Error Propagation отличается от throw?

Throw — это одноразовое действие выброса исключения. Error Propagation — весь процесс передачи ошибки через несколько уровней стека, от throw до catch. Propagation включает throw, автоматическую или ручную передачу через промежуточные функции и финальную обработку. Это более широкое понятие, описывающее жизненный цикл ошибки.

Как тестировать Error Propagation?

Используйте mock-объекты, которые выбрасывают исключения в заданных сценариях. Проверяйте, что функция корректно propagate или обрабатывает ошибку через assertThrows (Kotlin/JUnit) или XCTAssertThrowsError (Swift/XCTest). Для Result-based propagation проверяйте isSuccess/isError и значения в обоих кейсах.

Когда propagation через Result лучше исключений?

Result propagation предпочтительна для ожидаемых ошибок (неверные данные, бизнес-правила) в рамках одной архитектурной границы. Исключения лучше для неожиданных ошибок (потеря сети, ошибки ввода-вывода), которые должны быть обработаны на высоком уровне. Результат с ошибкой не прерывает поток выполнения, исключение — прерывает.

Как propagate ошибку через корутины Kotlin?

В Kotlin Coroutines исключение в launch автоматически propagate через CoroutineScope с отменой siblings. Используйте supervisorScope или SupervisorJob для изоляции: ошибка в одной корутине не отменяет другие. Для async ошибку нужно явно обработать через try-catch при вызове await(), иначе она будет проглочена.

Какой уровень должен быть конечным обработчиком ошибки?

Идеальный конечный обработчик — UI-слой (ViewController, Fragment/Composable). Только он имеет доступ к пользовательскому интерфейсу и может показать сообщение, Snackbar или диалог. Промежуточные слои (Repository, UseCase, ViewModel) propagate ошибку, трансформируя при необходимости в более абстрактный доменный тип.

Итоги

  • Error Propagation — передача ошибки вверх по стеку от места возникновения к обработчику через исключения или Result-контейнеры
  • Автоматическая propagation в Swift через throws не требует кода на промежуточных уровнях — ошибка поднимается сама
  • Ручная propagation в Kotlin через явный try-catch и throw на каждом уровне делает обработку осознанной, но многословной
  • Result Type передаёт ошибку как значение без раскрутки стека, удобен для ожидаемых ошибок в Clean Architecture
  • Правило handling: обрабатывай на уровне с контекстом (UI), propagate через уровни без контекста (domain, data)
  • Антипаттерны: пустой catch, потеря cause при трансформации, чрезмерная глубина propagation, игнорирование SupervisorJob
  • Проектируйте типизированные ошибки (sealed class / enum) для каждого слоя и трансформируйте их при пересечении границ слоёв

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также