Propagacja błędów: co to jest, mechanizm rozprzestrzeniania błędów i jak działa w programowaniu mobilnym

Autor: IT Sectr Opublikowano: 2026-05-26 Czas czytania: 9 min

Propagacja błędów — mechanizm rozprzestrzeniania błędu w górę stosu wywołań od miejsca wystąpienia do obsługi. Gdy funkcja nie może samodzielnie obsłużyć błędu, przekazuje go stronie wywołującej poprzez wyjątek (exception), deklarację throws lub typ zwracany. Prawidłowa implementacja propagacji jest krytyczna dla stabilności aplikacji mobilnych: nieobsłużone lub nieprawidłowo przekazane błędy prowadzą do awarii. Według Apple Swift Documentation (2026), automatyczna propagacja przez throws w Swift pozwala przekazywać błąd na dowolny poziom bez kodu boilerplate.

Najważniejsze

  • Propagacja błędów — przekazanie błędu od miejsca wystąpienia w górę stosu do obsługi, z pominięciem funkcji pośrednich
  • Automatyczna propagacja przez throws w Swift przekazuje błąd bez jawnego kodu na każdym poziomie stosu
  • Ręczna propagacja w Kotlin i Dart wymaga jawnego try-catch lub przekazania w kontenerze Result na każdym poziomie
  • Checked exceptions w Javie wymuszają propagację przez throws w sygnaturze, unchecked — pozwalają ignorować
  • Typ Result — alternatywa dla wyjątków, gdzie błąd jest przekazywany jako wartość bez rozwijania stosu

Czym jest propagacja błędów?

Propagacja błędów (rozprzestrzenianie błędu) — to proces przekazywania obiektu błędu od funkcji, w której powstał, w górę łańcucha wywołań do najbliższego odpowiedniego obsługiwacza. Wyobraź sobie stos wywołań: ViewController wywołuje ViewModel, ViewModel wywołuje Repository, Repository wywołuje API. Jeśli API zwraca błąd sieci, musi on przejść przez Repository i ViewModel do ViewController, który pokaże komunikat użytkownikowi. Każda funkcja pośrednia decyduje: obsłużyć błąd czy przekazać dalej (propagować).

Istnieją dwa podejścia do propagacji: automatyczne i ręczne. Przy podejściu automatycznym (Swift throws, Java checked exceptions) kompilator wymusza na programiście albo obsłużenie błędu, albo zadeklarowanie propagacji w sygnaturze. Przy podejściu ręcznym (typ Result, Kotlin Try) błąd jest przekazywany jako wartość — programista jawnie pisze kod przekazania lub transformacji błędu. Według Kotlin Result Docs (2026), Result<T> w Kotlin nie jest przeznaczony do propagacji przez granice funkcji bezpośrednio — należy go transformować lub obsługiwać na każdym poziomie, co czyni propagację bardziej świadomą, ale też bardziej rozwlekłą.

Wybór podejścia zależy od architektury aplikacji i języka. W Swift dominuje automatyczna propagacja przez throws, w Kotlin — mieszanka wyjątków (dla nieoczekiwanych błędów) i kontenerów podobnych do Result (dla oczekiwanych). Ważne jest zrozumienie: propagacja to nie cel, ale konieczność. Idealna architektura minimalizuje głębokość propagacji, obsługując błędy na najniższym możliwym poziomie, gdzie jest wystarczający kontekst do podjęcia decyzji.

Propagacja przez Throws w Swift

W Swift propagacja przez throws odbywa się automatycznie: jeśli funkcja A z throws wywołuje funkcję B z throws, a A nie obsługuje błędu B w do-catch, błąd jest automatycznie przekazywany stronie wywołującej A. To eliminuje kod boilerplate charakterystyczny dla Java checked exceptions, gdzie throws trzeba deklarować w każdej metodzie łańcucha. Swift stosuje zasadę „jedna funkcja throws w łańcuchu = cały łańcuch staje się throws, jeśli nie obsługuje się na poziomach pośrednich”.

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 — końcowy obsługiwacz
func onButtonTap() {
    let vm = UserViewModel()
    do {
        let user = try vm.loadUser(id: 42)
        updateUI(user)
    } catch {
        showError("Nie udało się załadować użytkownika")
    }
}

Łańcuch propagacji: networkService.request -> fetchUser -> loadUser -> onButtonTap. Każda funkcja pośrednia jest oznaczona throws i nie zawiera do-catch — błąd jest automatycznie przekazywany w górę. ViewController onButtonTap jest końcowym obsługiwaczem z do-catch. Gdyby ViewModel zdecydował się przekształcić błąd (opakować w inny typ), mógłby użyć do-catch i nowego throw. Automatyczna propagacja skraca kod: Repository nie musi wiedzieć, jak obsłużyć błąd — to odpowiedzialność ViewController, który ma dostęp do UI do pokazania komunikatu użytkownikowi.

Propagacja przez wyjątki w Kotlin

W Kotlin propagacja przez wyjątki nie wymaga deklaracji throws w sygnaturze (wszystkie wyjątki są unchecked). Wyjątek automatycznie wznosi się po stosie, aż napotka try-catch. Jednak brak throws w sygnaturze czyni propagację niejawną: programista nie widzi z sygnatury funkcji, że może ona rzucić wyjątek. To zaleta (mniej boilerplate) i wada (łatwiej zapomnieć o obsłudze) jednocześnie. Kotlin rozwiązuje ten problem przez konwencje i wzorce architektoniczne, a nie przez język.

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")
            }
        }
    }
}

W Repository propagacja z transformacją: przy IOException (sieć niedostępna) funkcja próbuje pobrać dane z pamięci podręcznej z bazy danych. Jeśli pamięć podręczna jest pusta, rzuca AppException — propagacja kontynuuje z nowym typem błędu. ViewModel przechwytuje AppException i tłumaczy na UiState.Error — błąd nie idzie dalej, propagacja zakończona na poziomie warstwy UI. Kotlin Coroutines dodają cechy szczególne: wyjątki w launch automatycznie rozprzestrzeniają się przez CoroutineExceptionHandler, a w async — tylko przy wywołaniu await(). Należy to uwzględnić przy projektowaniu propagacji w korutynach — SupervisorJob zapobiega anulowaniu korutyny nadrzędnej przy błędzie w potomnej.

Propagacja przez typ Result

Alternatywa dla wyjątków — propagacja przez typ-kontener, który przekazuje sukces lub błąd jako wartość. W tym podejściu funkcja zwraca nie wartość, ale opakowanie: Result<T, E> w Swift, Result<T> w Kotlin, Either<L, R> w Dart (z pakietu fpdart lub dartz). Błąd nie rozwija stosu — po prostu leży w kontenerze, a następny poziom decyduje, co z nim zrobić. To czyni propagację bardziej jawną i kontrolowaną.

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> — prosty kontener z polami data i error. Sealed class AppError definiuje typy błędów (Network, Auth). Funkcja fetchUser zwraca HttpResult, propagacja nie wymaga rozwijania stosu — strona wywołująca po prostu sprawdza isSuccess/isError. Takie podejście jest szczególnie przydatne w Clean Architecture, gdzie każda warstwa (data, domain, presentation) może przekształcać błąd: IOError -> DomainError -> UiError. Propagacja przez kontener czyni te transformacje jawnymi i testowalnymi, w przeciwieństwie do wyjątków, gdzie łańcuch przekształceń nie jest widoczny w sygnaturach funkcji.

Propagacja a obsługa: kiedy przekazywać, kiedy obsługiwać

Jedną z kluczowych decyzji w projektowaniu obsługi błędów jest wybór między propagacją (przekazanie wyżej) a obsługą (obsłużenie tutaj). Zasada podejmowania decyzji: obsługuj błąd na tym poziomie, gdzie jest wystarczający kontekst do znaczącego działania. Jeśli masz dostęp do UI — pokaż komunikat użytkownikowi. Jeśli masz dostęp do pamięci podręcznej — spróbuj odzyskać. Jeśli nie masz ani jednego, ani drugiego — propaguj.

ScenariuszDziałanieUzasadnienie
Błąd sieci w RepositoryPropagujRepository nie wie, czy użytkownik chce ponowić żądanie
Błąd parsowania w RepositoryObsłuż (zwróć wartość domyślną)Repository zna format, może zwrócić wartość zastępczą
Przekroczenie czasu w ViewModelObsłuż (UiState.Error)ViewModel zarządza UiState, wie, jak przetłumaczyć błąd
Błąd autoryzacji w InterceptorObsłuż (odśwież token)Interceptor ma dostęp do tokenów i może przywrócić sesję
Nieznany błąd w UseCasePropagujUseCase nie ma kontekstu UI — tylko logika biznesowa

Złota zasada: minimum propagacji, maksimum obsługi na niższych poziomach. Jeśli Repository może odzyskać dane z pamięci podręcznej — powinno to zrobić, nie przekazując błędu wyżej. Jeśli ViewModel może pokazać Snackbar — niech pokaże, nie wymagając od ViewController dodatkowego kodu. Każdy poziom propagacji zwiększa powiązanie i utrudnia testowanie. Według Google Android Architecture Guide (2026), zaleca się minimalizowanie propagacji przez granice warstw, używając sealed class UiState do reprezentowania wszystkich możliwych stanów (Loading, Success, Error) na poziomie ViewModel i nieprzekazywanie wyjątków bezpośrednio do warstwy UI.

Problemy i antywzorce propagacji błędów

Nieprawidłowa propagacja jest źródłem trudnych do wyśledzenia błędów w aplikacjach mobilnych. Rozważmy pięć głównych problemów, z którymi spotykają się programiści, oraz sposoby ich rozwiązania.

Utrata kontekstu błędu

Najczęstszy problem: podczas propagacji wyjątek jest przechwytywany, logowany i rzucany nowy bez oryginalnego wyjątku. Programista traci StackTrace i nie może zrozumieć, gdzie dokładnie wystąpił błąd. W Swift używaj error chaining: throw MyError(context: originalError). W Kotlin: throw AppException(cause = originalException). W Dart: throw AppException(message, originalException). Nigdy nie twórz nowego wyjątku bez przekazania przyczyny/błędu źródłowego.

Ignorowanie błędu (pusty catch)

catch (e: Exception) { /* nic */ } — antywzorzec, który prowadzi do tego, że aplikacja kontynuuje działanie w nieprawidłowym stanie. Jeśli jesteś pewien, że błąd można zignorować — dodaj komentarz z uzasadnieniem. W Swift do opcjonalnego ignorowania używaj try? (błąd -> nil). W Kotlin — Result<T>.onFailure { /* log */ }. Nie wyciszaj wyjątków bez logowania.

Nadmierna głębokość propagacji

Jeśli błąd przechodzi przez 5+ poziomów bez obsługi, architektura wymaga przeglądu. Każdy poziom propagacji to zależność od sygnatury throws funkcji leżących niżej. Rozwiązanie: używaj kontenerów Failure (sealed class Result { Success, Error }) na granicach warstw, aby propagacja była jawna i ograniczona. Im krótszy łańcuch propagacji, tym łatwiej testować i debugować kod.

Propagacja w korutynach bez SupervisorJob

W Kotlin Coroutines wyjątek w launch domyślnie anuluje korutynę nadrzędną i wszystkie siblings (potomne tego samego scope). Jeśli jedno z 10 równoległych zadań upadnie, pozostałe 9 zostanie anulowanych, co często jest niepożądane. Używaj SupervisorJob lub supervisorScope do izolacji błędów: błąd w jednym child nie anuluje siblings. ViewModelScope domyślnie używa SupervisorJob, co chroni przed tym problemem w Androidzie.

Propagacja przez callback bez obsługi

W API opartym na callback błąd często jest przekazywany jako parametr callbacka. Jeśli callback nie obsługuje błędu (lub obsługuje nieprawidłowo), propagacja staje się niejawna i łatwo się gubi. Rozwiązanie: migruj na async/await (Swift) lub korutyny (Kotlin), gdzie propagacja działa przez standardowe mechanizmy try-catch. Jeśli callback jest nieunikniony — używaj Either<Error, T> lub Result<T> do wymuszenia obsługi obu przypadków.

Często zadawane pytania

Czym różni się propagacja błędów od throw?

Throw to jednorazowe działanie wyrzucenia wyjątku. Propagacja błędów to cały proces przekazywania błędu przez kilka poziomów stosu, od throw do catch. Propagacja obejmuje throw, automatyczne lub ręczne przekazywanie przez funkcje pośrednie i końcową obsługę. To szersze pojęcie opisujące cykl życia błędu.

Jak testować propagację błędów?

Używaj obiektów mock, które rzucają wyjątki w zadanych scenariuszach. Sprawdzaj, czy funkcja prawidłowo propaguje lub obsługuje błąd przez assertThrows (Kotlin/JUnit) lub XCTAssertThrowsError (Swift/XCTest). Dla propagacji opartej na Result sprawdzaj isSuccess/isError i wartości w obu przypadkach.

Kiedy propagacja przez Result jest lepsza od wyjątków?

Propagacja przez Result jest preferowana dla oczekiwanych błędów (nieprawidłowe dane, reguły biznesowe) w ramach jednej granicy architektonicznej. Wyjątki są lepsze dla nieoczekiwanych błędów (utrata sieci, błędy wejścia-wyjścia), które powinny być obsłużone na wysokim poziomie. Wynik z błędem nie przerywa przepływu wykonania, wyjątek — przerywa.

Jak propagować błąd przez korutyny Kotlin?

W Kotlin Coroutines wyjątek w launch automatycznie propaguje przez CoroutineScope z anulowaniem siblings. Używaj supervisorScope lub SupervisorJob do izolacji: błąd w jednej korutynie nie anuluje innych. Dla async błąd należy jawnie obsłużyć przez try-catch przy wywołaniu await(), w przeciwnym razie zostanie połknięty.

Jaki poziom powinien być końcowym obsługiwaczem błędu?

Idealny końcowy obsługiwacz to warstwa UI (ViewController, Fragment/Composable). Tylko ona ma dostęp do interfejsu użytkownika i może pokazać komunikat, Snackbar lub dialog. Warstwy pośrednie (Repository, UseCase, ViewModel) propagują błąd, przekształcając go w razie potrzeby na bardziej abstrakcyjny typ domenowy.

Podsumowanie

  • Propagacja błędów — przekazanie błędu w górę stosu od miejsca wystąpienia do obsługiwacza przez wyjątki lub kontenery Result
  • Automatyczna propagacja w Swift przez throws nie wymaga kodu na poziomach pośrednich — błąd wznosi się sam
  • Ręczna propagacja w Kotlin przez jawny try-catch i throw na każdym poziomie czyni obsługę świadomą, ale rozwlekłą
  • Typ Result przekazuje błąd jako wartość bez rozwijania stosu, wygodny dla oczekiwanych błędów w Clean Architecture
  • Zasada obsługi: obsługuj na poziomie z kontekstem (UI), propaguj przez poziomy bez kontekstu (domain, data)
  • Antywzorce: pusty catch, utrata przyczyny przy transformacji, nadmierna głębokość propagacji, ignorowanie SupervisorJob
  • Projektuj typowane błędy (sealed class / enum) dla każdej warstwy i przekształcaj je przy przekraczaniu granic warstw

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również