Propagace chyb — mechanismus šíření chyby nahoru zásobníkem volání od místa vzniku k obsluze. Když funkce nemůže chybu zpracovat sama, předá ji volající straně pomocí výjimky (exception), deklarace throws nebo návratového typu. Správná implementace propagace je kritická pro stabilitu mobilních aplikací: neošetřené nebo nesprávně předané chyby vedou k pádům. Podle Apple Swift Documentation (2026) umožňuje automatická propagace pomocí throws v Swift předávat chybu na libovolnou úroveň bez boilerplate kódu.
Hlavní body
Propagace chyb — proces předání objektu chyby od funkce, kde vznikla, nahoru řetězcem volání k nejbližšímu vhodnému obslužnému programu. Představte si zásobník volání: ViewController volá ViewModel, ViewModel volá Repository, Repository volá API. Pokud API vrátí chybu sítě, musí projít přes Repository a ViewModel k ViewController, který zobrazí zprávu uživateli. Každá mezilehlá funkce rozhoduje: zpracovat chybu nebo předat dál (propagovat).
Existují dva přístupy k propagaci: automatický a ruční. Při automatickém přístupu (Swift throws, Java kontrolované výjimky) compiler nutí vývojáře buď chybu zpracovat, nebo deklarovat propagaci v signatuře. Při ručním přístupu (typ Result, Kotlin Try) je chyba předávána jako hodnota — vývojář explicitně píše kód pro předání nebo transformaci chyby. Podle Kotlin Result Docs (2026) není Result<T> v Kotlin určen pro přímou propagaci přes hranice funkcí — musí být transformován nebo zpracován na každé úrovni, což činí propagaci vědomější, ale také rozvláčnější.
Výběr přístupu závisí na architektuře aplikace a jazyce. V Swift dominuje automatická propagace pomocí throws, v Kotlin — směs výjimek (pro neočekávané chyby) a kontejnerů podobných Result (pro očekávané chyby). Je důležité pochopit: propagace není cíl, ale nutnost. Ideální architektura minimalizuje hloubku propagace tím, že zpracovává chyby na nejnižší možné úrovni, kde je dostatek kontextu pro rozhodnutí.
V Swift probíhá propagace pomocí throws automaticky: pokud funkce A s throws volá funkci B s throws a A nezpracovává chybu B v do-catch, chyba je automaticky předána volající straně A. To eliminuje boilerplate kód charakteristický pro Java kontrolované výjimky, kde musí být throws deklarováno v každé metodě řetězce. Swift používá princip „jedna throws funkce v řetězci = celý řetězec se stává throws, pokud není zpracováváno na mezilehlých úrovních”.
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 — konečný zpracovatel
func onButtonTap() {
let vm = UserViewModel()
do {
let user = try vm.loadUser(id: 42)
updateUI(user)
} catch {
showError("Načtení uživatele selhalo")
}
}
Řetězec propagace: networkService.request -> fetchUser -> loadUser -> onButtonTap. Každá mezilehlá funkce je označena throws a neobsahuje do-catch — chyba je automaticky předávána nahoru. ViewController onButtonTap je konečný zpracovatel s do-catch. Pokud by se ViewModel rozhodl chybu transformovat (zabalit do jiného typu), mohl by použít do-catch a nový throw. Automatická propagace zkracuje kód: Repository nemusí vědět, jak chybu zpracovat — to je odpovědnost ViewController, který má přístup k UI pro zobrazení zprávy uživateli.
V Kotlin propagace pomocí výjimek nevyžaduje deklaraci throws v signatuře (všechny výjimky jsou nekontrolované). Výjimka automaticky stoupá zásobníkem, dokud nenarazí na try-catch. Absence throws v signatuře však činí propagaci implicitní: vývojář z signatury funkce nevidí, že může vyhodit výjimku. To je jak výhoda (méně boilerplate), tak nevýhoda (snadněji zapomenout zpracovat). Kotlin řeší tento problém pomocí konvencí a architektonických vzorů, nikoli pomocí jazyka.
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")
}
}
}
}
V Repository propagace s transformací: při IOException (síť nedostupná) se funkce pokusí získat data z mezipaměti v databázi. Pokud je mezipaměť prázdná, vyhodí AppException — propagace pokračuje s novým typem chyby. ViewModel zachytí AppException a převede jej na UiState.Error — chyba nejde dále, propagace končí na úrovni vrstvy UI. Kotlin Coroutines přidávají zvláštnosti: výjimky v launch se automaticky šíří přes CoroutineExceptionHandler a v async — pouze při volání await(). Je důležité to zohlednit při návrhu propagace v korutinách — SupervisorJob zabraňuje zrušení nadřazené korutiny při chybě v podřízené korutině.
Alternativa k výjimkám — propagace pomocí typového kontejneru, který předává úspěch nebo chybu jako hodnotu. V tomto přístupu funkce nevrací hodnotu, ale obal: Result<T, E> v Swift, Result<T> v Kotlin, Either<L, R> v Dart (z balíčku fpdart nebo dartz). Chyba nerozbalí zásobník — jednoduše leží v kontejneru a další úroveň rozhodne, co s ní. To činí propagaci explicitnější a ovladatelnější.
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> — jednoduchý kontejner s poli data a error. Sealed class AppError definuje typy chyb (Network, Auth). Funkce fetchUser vrací HttpResult, propagace nevyžaduje rozbalení zásobníku — volající strana jednoduše kontroluje isSuccess/isError. Tento přístup je zvláště užitečný v Clean Architecture, kde každá vrstva (data, domain, presentation) může transformovat chybu: IOError -> DomainError -> UiError. Propagace pomocí kontejneru činí tyto transformace explicitními a testovatelnými, na rozdíl od výjimek, kde řetězec transformací není viditelný v signaturách funkcí.
Jedním z klíčových rozhodnutí při návrhu zpracování chyb je volba mezi propagací (předání výše) a zpracováním (zpracování zde). Pravidlo rozhodování: zpracuj chybu na té úrovni, kde je dostatek kontextu pro smysluplnou akci. Pokud máš přístup k UI — zobraz zprávu uživateli. Pokud máš přístup k mezipaměti — zkus se zotavit. Pokud nemáš ani jedno — propaguj.
| Scénář | Akce | Odůvodnění |
|---|---|---|
| Chyba sítě v Repository | Propaguj | Repository neví, zda uživatel chce opakovat požadavek |
| Chyba parsování v Repository | Zpracuj (vrať výchozí hodnotu) | Repository zná formát, může vrátit záložní hodnotu |
| Časový limit v ViewModel | Zpracuj (UiState.Error) | ViewModel spravuje UiState, ví jak převést chybu |
| Chyba autorizace v Interceptor | Zpracuj (obnov token) | Interceptor má přístup k tokenům a může obnovit relaci |
| Neznámá chyba v UseCase | Propaguj | UseCase nemá UI kontext — pouze obchodní logiku |
Zlaté pravidlo: minimum propagace, maximum zpracování na nižších úrovních. Pokud se Repository může zotavit z mezipaměti — měl by to udělat, aniž by předával chybu výše. Pokud ViewModel může zobrazit Snackbar — ať ho zobrazí, aniž by vyžadoval další kód od ViewController. Každá úroveň propagace zvyšuje provázanost a komplikuje testování. Podle Google Android Architecture Guide (2026) se doporučuje minimalizovat propagaci přes hranice vrstev pomocí sealed class UiState k reprezentaci všech možných stavů (Loading, Success, Error) na úrovni ViewModel a nepředávat výjimky přímo do vrstvy UI.
Nesprávná propagace je zdrojem obtížně odhalitelných chyb v mobilních aplikacích. Podívejme se na pět hlavních problémů, se kterými se vývojáři setkávají, a způsoby jejich řešení.
Nejčastější problém: při propagaci je výjimka zachycena, zalogována a je vyhozena nová bez původní výjimky. Vývojář ztrácí StackTrace a nemůže pochopit, kde přesně chyba vznikla. V Swift použijte řetězení chyb: throw MyError(context: originalError). V Kotlin: throw AppException(cause = originalException). V Dart: throw AppException(message, originalException). Nikdy nevytvářejte novou výjimku bez předání příčiny/základní chyby.
catch (e: Exception) { /* nic */ } — antipravidlo, které vede k tomu, že aplikace pokračuje v činnosti v nesprávném stavu. Pokud jste si jisti, že chybu lze ignorovat — přidejte komentář s odůvodněním. V Swift pro volitelné ignorování použijte try? (chyba -> nil). V Kotlin — Result<T>.onFailure { /* log */ }. Neutlumujte výjimky bez logování.
Pokud chyba prochází 5+ úrovněmi bez zpracování, architektura vyžaduje přehodnocení. Každá úroveň propagace je závislost na throws signatuře podřazených funkcí. Řešení: použijte Failure kontejnery (sealed class Result { Success, Error }) na hranicích vrstev, aby byla propagace explicitní a omezená. Čím kratší je řetězec propagace, tím snazší je testovat a ladit kód.
V Kotlin Coroutines výjimka v launch ve výchozím nastavení ruší nadřazenou korutinu a všechny sourozence (děti stejného scope). Pokud jedna z 10 paralelních úloh selže, zbývajících 9 bude zrušeno, což je často nežádoucí. Použijte SupervisorJob nebo supervisorScope pro izolaci chyb: chyba v jednom child neruší sourozence. ViewModelScope ve výchozím nastavení používá SupervisorJob, což v Android chrání před tímto problémem.
V API založeném na callback je chyba často předávána jako parametr callbacku. Pokud callback chybu nezpracovává (nebo zpracovává nesprávně), propagace se stává implicitní a snadno se ztrácí. Řešení: migrujte na async/await (Swift) nebo korutiny (Kotlin), kde propagace funguje přes standardní mechanismy try-catch. Pokud je callback nevyhnutelný — použijte Either<Error, T> nebo Result<T> pro vynucené zpracování obou případů.
Často kladené otázky
Throw je jednorázová akce vyhození výjimky. Propagace chyb je celý proces předávání chyby přes několik úrovní zásobníku, od throw po catch. Propagace zahrnuje throw, automatické nebo ruční předávání přes mezilehlé funkce a konečné zpracování. Je to širší koncept popisující životní cyklus chyby.
Použijte mock objekty, které vyhazují výjimky v daných scénářích. Zkontrolujte, že funkce správně propaguje nebo zpracovává chybu pomocí assertThrows (Kotlin/JUnit) nebo XCTAssertThrowsError (Swift/XCTest). Pro propagaci založenou na Result kontrolujte isSuccess/isError a hodnoty v obou případech.
Propagace pomocí Result je preferována pro očekávané chyby (neplatná data, obchodní pravidla) v rámci jedné architektonické hranice. Výjimky jsou lepší pro neočekávané chyby (ztráta sítě, I/O chyby), které by měly být zpracovány na vysoké úrovni. Výsledek s chybou nepřerušuje tok provádění, výjimka ho přerušuje.
V Kotlin Coroutines se výjimka v launch automaticky propaguje přes CoroutineScope se zrušením sourozenců. Použijte supervisorScope nebo SupervisorJob pro izolaci: chyba v jedné korutině neruší ostatní. Pro async musí být chyba explicitně zpracována pomocí try-catch při volání await(), jinak bude pohlcena.
Ideální konečný zpracovatel je vrstva UI (ViewController, Fragment/Composable). Pouze ta má přístup k uživatelskému rozhraní a může zobrazit zprávu, Snackbar nebo dialog. Mezilehlé vrstvy (Repository, UseCase, ViewModel) propagují chybu a v případě potřeby ji transformují do abstraktnějšího doménového typu.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také