Error Propagation — це механізм поширення помилки вгору по стеку викликів від місця виникнення до обробника. Коли функція не може обробити помилку самостійно, вона передає її стороні, що викликає, через виняток (exception), throws-декларацію або Return Type. Правильна реалізація propagation критична для стабільності мобільних застосунків: необроблені або неправильно передані помилки призводять до крашів. За даними Apple Swift Documentation (2026), автоматичне поширення через throws у Swift дозволяє передавати помилку на будь-який рівень без шаблонного коду.
Головне
Error Propagation (поширення помилки) — це процес передачі об'єкта помилки від функції, де вона виникла, вгору по ланцюжку викликів до найближчого відповідного обробника. Уявіть стек викликів: ViewController викликає ViewModel, ViewModel викликає Repository, Repository викликає API. Якщо API повертає мережеву помилку, вона має пройти через Repository та ViewModel до ViewController, який покаже повідомлення користувачеві. Кожна проміжна функція вирішує: обробити помилку або передати далі (propagate).
Існує два підходи до поширення: автоматичний і ручний. При автоматичному підході (Swift throws, Java checked exceptions) компілятор примушує розробника або обробити помилку, або декларувати поширення в сигнатурі. При ручному підході (Result Type, Kotlin Try) помилка передається як значення — розробник явно пише код для передачі або трансформації помилки. За даними Kotlin Result Docs (2026), Result<T> у Kotlin не призначений для поширення через межі функцій безпосередньо — його потрібно трансформувати або обробляти на кожному рівні, що робить поширення більш усвідомленим, але й більш багатослівним.
Вибір підходу залежить від архітектури застосунку та мови. У Swift домінує автоматичне поширення через throws, у Kotlin — мікс винятків (для неочікуваних помилок) та Result-подібних контейнерів (для очікуваних). Важливо розуміти: поширення — це не мета, а необхідність. Ідеальна архітектура мінімізує глибину поширення, обробляючи помилки на максимально низькому рівні, де є достатньо контексту для прийняття рішення.
У Swift поширення через throws відбувається автоматично: якщо функція A з throws викликає функцію B з throws, і A не обробляє помилку B у do-catch, помилка автоматично передається стороні, що викликає A. Це позбавляє шаблонного коду, характерного для Java checked exceptions, де throws потрібно декларувати в кожному методі ланцюжка. Swift використовує принцип «одна throws-функція в ланцюжку = весь ланцюжок стає throws, якщо не обробляти на проміжних рівнях».
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("Не вдалося завантажити користувача")
}
}
Ланцюжок поширення: networkService.request -> fetchUser -> loadUser -> onButtonTap. Кожна проміжна функція позначена throws і не містить do-catch — помилка автоматично передається вгору. ViewController onButtonTap — кінцевий обробник з do-catch. Якби ViewModel вирішила трансформувати помилку (загорнути в інший тип), вона могла б використати do-catch і новий throw. Автоматичне поширення скорочує код: Repository не потрібно знати, як обробляти помилку — це відповідальність ViewController, який має доступ до UI для показу повідомлення користувачеві.
У Kotlin поширення через винятки не вимагає декларації throws у сигнатурі (всі винятки unchecked). Виняток автоматично піднімається по стеку, доки не зустріне try-catch. Однак відсутність throws у сигнатурі робить поширення неявним: розробник не бачить із сигнатури функції, що вона може викинути виняток. Це плюс (менше шаблонного коду) і мінус (легше забути обробити) одночасно. 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 поширення з трансформацією: при IOException (мережа недоступна) функція намагається отримати кешовані дані з бази даних. Якщо кеш порожній, викидає AppException — поширення продовжується вже з новим типом помилки. ViewModel перехоплює AppException і транслює в UiState.Error — помилка не йде далі, поширення завершено на рівні UI-шару. Kotlin Coroutines додають особливості: винятки в launch автоматично поширюються через CoroutineExceptionHandler, а в async — лише при виклику await(). Це важливо враховувати при проєктуванні поширення в корутинах — SupervisorJob запобігає скасуванню батьківської корутини при помилці в дочірній.
Альтернатива виняткам — поширення через тип-контейнер, який передає успіх або помилку як значення. У цьому підході функція повертає не значення, а обгортку: Result<T, E> у Swift, Result<T> у Kotlin, Either<L, R> у Dart (із пакета fpdart або dartz). Помилка не розкручує стек — вона просто лежить у контейнері, і наступний рівень вирішує, що з нею робити. Це робить поширення більш явним і контрольованим.
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, поширення не вимагає розкрутки стеку — сторона, що викликає, просто перевіряє isSuccess/isError. Такий підхід особливо корисний у Clean Architecture, де кожен шар (data, domain, presentation) може трансформувати помилку: IOError -> DomainError -> UiError. Поширення через контейнер робить ці трансформації явними та тестованими, на відміну від винятків, де ланцюжок перетворень не видно в сигнатурах функцій.
Одне з ключових рішень у проєктуванні обробки помилок — вибір між поширенням (передати вище) та обробкою (обробити тут). Правило прийняття рішення: обробляй помилку на тому рівні, де є достатньо контексту для значущої дії. Якщо в тебе є доступ до UI — покажи повідомлення користувачеві. Якщо є доступ до кешу — спробуй відновитися. Якщо немає ні того, ні іншого — propagate.
| Сценарій | Дія | Обґрунтування |
|---|---|---|
| Мережева помилка в Repository | Propagate | Repository не знає, чи хоче користувач повторити запит |
| Помилка парсингу в Repository | Обробити (повернути default) | Repository знає формат, може повернути fallback-значення |
| Таймаут у ViewModel | Обробити (UiState.Error) | ViewModel керує UiState, знає, як транслювати помилку |
| Помилка авторизації в Interceptor | Обробити (refresh token) | Interceptor має доступ до токенів і може відновити сесію |
| Невідома помилка в UseCase | Propagate | UseCase не має UI-контексту — лише бізнес-логіка |
Золоте правило: мінімум поширення, максимум обробки на нижніх рівнях. Якщо Repository може відновитися з кешу — він має це зробити, не передаючи помилку вище. Якщо ViewModel може показати Snackbar — хай покаже, не вимагаючи від ViewController додаткового коду. Кожен рівень поширення збільшує зв'язність та ускладнює тестування. За даними Google Android Architecture Guide (2026), рекомендується мінімізувати поширення через межі шарів, використовуючи sealed class UiState для представлення всіх можливих станів (Loading, Success, Error) на рівні ViewModel, і не передавати винятки в UI-шар безпосередньо.
Некоректне поширення — джерело важко відловлюваних багів у мобільних застосунках. Розглянемо п'ять основних проблем, з якими стикаються розробники, та способи їх вирішення.
Найчастіша проблема: при поширенні виняток перехоплюється, логується та викидається новий без оригінального винятку. Розробник втрачає StackTrace і не може зрозуміти, де саме виникла помилка. У Swift використовуйте error chaining: throw MyError(context: originalError). У Kotlin: throw AppException(cause = originalException). У Dart: throw AppException(message, originalException). Ніколи не створюйте новий виняток без передачі cause/underlyingError.
catch (e: Exception) { /* нічого */ } — антипатерн, який призводить до того, що застосунок продовжує роботу в некоректному стані. Якщо ви впевнені, що помилку можна ігнорувати — додайте коментар з обґрунтуванням. У Swift для опціонального ігнорування використовуйте try? (помилка -> nil). У Kotlin — Result<T>.onFailure { /* log */ }. Не глушіть винятки без логування.
Якщо помилка проходить через 5+ рівнів без обробки, архітектура потребує перегляду. Кожен рівень поширення — це залежність від сигнатури throws нижчележачих функцій. Рішення: використовуйте Failure-контейнери (sealed class Result { Success, Error }) на межах шарів, щоб зробити поширення явним і обмеженим. Чим коротший ланцюжок поширення, тим простіше тестувати та налагоджувати код.
У Kotlin Coroutines виняток у launch за замовчуванням скасовує батьківську корутину та всіх siblings (дітей того ж scope). Якщо одне з 10 паралельних завдань впало, інші 9 будуть скасовані, що часто небажано. Використовуйте SupervisorJob або supervisorScope для ізоляції помилок: помилка в одному child не скасовує siblings. ViewModelScope використовує SupervisorJob за замовчуванням, що рятує від цієї проблеми в Android.
У callback-based API помилка часто передається як параметр колбеку. Якщо callback не обробляє помилку (або обробляє некоректно), поширення стає неявним і легко втрачається. Рішення: мігруйте на async/await (Swift) або корутини (Kotlin), де поширення працює через стандартні механізми try-catch. Якщо callback неминучий — використовуйте Either<Error, T> або Result<T> для примусової обробки обох кейсів.
Часті запитання
Throw — це одноразова дія викиду винятку. Error Propagation — весь процес передачі помилки через кілька рівнів стеку, від throw до catch. Поширення включає throw, автоматичну або ручну передачу через проміжні функції та фінальну обробку. Це більш широке поняття, що описує життєвий цикл помилки.
Використовуйте mock-об'єкти, які викидають винятки в заданих сценаріях. Перевіряйте, що функція коректно propagate або обробляє помилку через assertThrows (Kotlin/JUnit) або XCTAssertThrowsError (Swift/XCTest). Для Result-based propagation перевіряйте isSuccess/isError та значення в обох кейсах.
Result propagation краща для очікуваних помилок (неправильні дані, бізнес-правила) в межах однієї архітектурної межі. Винятки кращі для неочікуваних помилок (втрата мережі, помилки введення-виведення), які мають бути оброблені на високому рівні. Результат з помилкою не перериває потік виконання, виняток — перериває.
У Kotlin Coroutines виняток у launch автоматично propagate через CoroutineScope зі скасуванням siblings. Використовуйте supervisorScope або SupervisorJob для ізоляції: помилка в одній корутині не скасовує інші. Для async помилку потрібно явно обробити через try-catch при виклику await(), інакше вона буде проковтнута.
Ідеальний кінцевий обробник — UI-шар (ViewController, Fragment/Composable). Тільки він має доступ до інтерфейсу користувача і може показати повідомлення, Snackbar або діалог. Проміжні шари (Repository, UseCase, ViewModel) propagate помилку, трансформуючи при потребі в більш абстрактний доменний тип.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також