Пропагация на грешки: какво е, механизъм на разпространение на грешки и как работи в мобилната разработка

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

Пропагация на грешки — механизъм за разпространение на грешката нагоре по стека на извиквания от мястото на възникване до обработчика. Когато функцията не може да обработи грешката самостоятелно, тя я предава на извикващата страна чрез изключение (exception), throws декларация или тип на връщане. Правилната имплементация на пропагацията е критична за стабилността на мобилните приложения: необработените или неправилно предадени грешки водят до сривове. Според Apple Swift Documentation (2026), автоматичната пропагация чрез throws в Swift позволява предаване на грешката на всяко ниво без boilerplate код.

Основни точки

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

Какво е пропагация на грешки?

Пропагация на грешки — процес на предаване на обекта на грешка от функцията, където е възникнал, нагоре по веригата на извиквания до най-близкия подходящ обработчик. Представете си стека на извиквания: ViewController извиква ViewModel, ViewModel извиква Repository, Repository извиква API. Ако API върне мрежова грешка, тя трябва да премине през Repository и ViewModel до ViewController, който ще покаже съобщение на потребителя. Всяка междинна функция решава: да обработи грешката или да я предаде по-нататък (да я пропагира).

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

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

Пропагация чрез Throws в Swift

В Swift пропагацията чрез 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("Неуспешно зареждане на потребителя")
    }
}

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

Пропагация чрез изключения в Kotlin

В Kotlin пропагацията чрез изключения не изисква декларация на throws в сигнатурата (всички изключения са unchecked). Изключението автоматично се издига нагоре по стека, докато не срещне try-catch. Липсата на throws в сигнатурата обаче прави пропагацията неявна: разработчикът не вижда от сигнатурата на функцията, че тя може да хвърли изключение. Това е едновременно предимство (по-малко 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 пропагация с трансформация: при IOException (мрежата е недостъпна) функцията се опитва да получи кеширани данни от базата данни. Ако кешът е празен, хвърля AppException — пропагацията продължава с нов тип грешка. ViewModel прихваща AppException и го превежда в UiState.Error — грешката не отива по-нататък, пропагацията приключва на ниво UI слой. Kotlin Coroutines добавят особености: изключенията в launch се разпространяват автоматично чрез CoroutineExceptionHandler, а в async — само при извикване на await(). Важно е да се вземе предвид това при проектиране на пропагация в корутини — SupervisorJob предотвратява анулиране на родителската корутина при грешка в дъщерна корутина.

Пропагация чрез Result тип

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

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, пропагацията не изисква развиване на стека — извикващата страна просто проверява isSuccess/isError. Този подход е особено полезен в Clean Architecture, където всеки слой (data, domain, presentation) може да трансформира грешката: IOError -> DomainError -> UiError. Пропагацията чрез контейнер прави тези трансформации изрични и тестваеми, за разлика от изключенията, където веригата на трансформации не се вижда в сигнатурите на функциите.

Пропагация vs Обработка: кога да предаваме, кога да обработваме

Едно от ключовите решения при проектиране на обработката на грешки е изборът между пропагация (предаване нагоре) и обработка (обработване тук). Правило за вземане на решение: обработвай грешката на това ниво, където има достатъчно контекст за смислено действие. Ако имаш достъп до UI — покажи съобщение на потребителя. Ако имаш достъп до кеш — опитай се да възстановиш. Ако нямаш нито едното, нито другото — пропагирай.

СценарийДействиеОбосновка
Мрежова грешка в RepositoryПропагирайRepository не знае дали потребителят иска да повтори заявката
Грешка при парсиране в RepositoryОбработи (върни стойност по подразбиране)Repository знае формата, може да върне резервна стойност
Таймаут в ViewModelОбработи (UiState.Error)ViewModel управлява UiState, знае как да преведе грешката
Грешка при авторизация в InterceptorОбработи (обнови токен)Interceptor има достъп до токени и може да възстанови сесията
Неизвестна грешка в UseCaseПропагирайUseCase няма UI контекст — само бизнес логика

Златно правило: минимум пропагация, максимум обработка на по-ниските нива. Ако Repository може да се възстанови от кеша — трябва да го направи, без да предава грешката нагоре. Ако ViewModel може да покаже Snackbar — нека го покаже, без да изисква допълнителен код от ViewController. Всяко ниво на пропагация увеличава обвързаността и затруднява тестването. Според Google Android Architecture Guide (2026) се препоръчва минимизиране на пропагацията през границите на слоеве, като се използва sealed class UiState за представяне на всички възможни състояния (Loading, Success, Error) на ниво ViewModel и не се предават изключения директно в UI слоя.

Проблеми и анти-модели на пропагация на грешки

Неправилната пропагация е източник на трудно откриваеми грешки в мобилните приложения. Нека разгледаме пет основни проблема, с които се сблъскват разработчиците, и начините за тяхното решаване.

Загуба на контекст на грешката

Най-честият проблем: при пропагация изключението се прихваща, логва се и се хвърля ново без оригиналното изключение. Разработчикът губи StackTrace и не може да разбере къде точно е възникнала грешката. В Swift използвайте верижно свързване на грешки: throw MyError(context: originalError). В Kotlin: throw AppException(cause = originalException). В Dart: throw AppException(message, originalException). Никога не създавайте ново изключение без предаване на причината/основната грешка.

Игнориране на грешката (празен catch)

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

Прекомерна дълбочина на пропагация

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

Пропагация в корутини без SupervisorJob

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

Пропагация чрез callback без обработка

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

Често задавани въпроси

Как се различава пропагацията на грешки от throw?

Throw е еднократно действие за хвърляне на изключение. Пропагацията на грешки е целият процес на предаване на грешката през няколко нива на стека, от throw до catch. Пропагацията включва throw, автоматично или ръчно предаване чрез междинни функции и крайна обработка. Това е по-широко понятие, описващо жизнения цикъл на грешката.

Как да тестваме пропагация на грешки?

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

Кога пропагацията чрез Result е по-добра от изключенията?

Result пропагацията е за предпочитане за очаквани грешки (невалидни данни, бизнес правила) в рамките на една архитектурна граница. Изключенията са по-добри за неочаквани грешки (загуба на мрежа, I/O грешки), които трябва да бъдат обработени на високо ниво. Резултат с грешка не прекъсва потока на изпълнение, изключението го прекъсва.

Как да пропагираме грешка чрез Kotlin корутини?

В Kotlin Coroutines изключението в launch автоматично се пропагира чрез CoroutineScope с анулиране на sibling. Използвайте supervisorScope или SupervisorJob за изолиране: грешка в една корутина не анулира другите. За async грешката трябва изрично да се обработи чрез try-catch при извикване на await(), в противен случай ще бъде погълната.

Кое ниво трябва да бъде крайният обработчик на грешката?

Идеалният краен обработчик е UI слоят (ViewController, Fragment/Composable). Само той има достъп до потребителския интерфейс и може да покаже съобщение, Snackbar или диалог. Междинните слоеве (Repository, UseCase, ViewModel) пропагират грешката, трансформирайки я при необходимост в по-абстрактен домейн тип.

Заключение

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

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

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също