Foutpropagatie — het mechanisme van foutverspreiding omhoog door de aanroepstack van de plaats van ontstaan tot de afhandelaar. Wanneer een functie een fout niet zelf kan afhandelen, geeft deze deze door aan de aanroepende partij via een uitzondering (exception), een throws-declaratie of het retourtype. Correcte implementatie van propagatie is cruciaal voor de stabiliteit van mobiele applicaties: onverwerkte of onjuist doorgegeven fouten leiden tot crashes. Volgens Apple Swift Documentation (2026) maakt automatische propagatie via throws in Swift het mogelijk om een fout naar elk niveau door te geven zonder boilerplate-code.
Belangrijkste punten
Foutpropagatie — het proces van het doorgeven van een foutobject van de functie waar het is ontstaan omhoog door de aanroepketen naar de dichtstbijzijnde geschikte afhandelaar. Stel u de aanroepstack voor: ViewController roept ViewModel aan, ViewModel roept Repository aan, Repository roept API aan. Als de API een netwerkfout retourneert, moet deze door Repository en ViewModel naar ViewController gaan, die een bericht aan de gebruiker zal tonen. Elke tussenliggende functie beslist: de fout afhandelen of doorgeven (propageren).
Er zijn twee benaderingen van propagatie: automatisch en handmatig. Bij de automatische benadering (Swift throws, Java checked exceptions) dwingt de compiler de ontwikkelaar om de fout te behandelen of propagatie in de handtekening te declareren. Bij de handmatige benadering (Result Type, Kotlin Try) wordt de fout als waarde doorgegeven — de ontwikkelaar schrijft expliciet code voor het doorgeven of transformeren van de fout. Volgens Kotlin Result Docs (2026) is Result<T> in Kotlin niet bedoeld voor directe propagatie over functiegrenzen heen — het moet op elk niveau worden getransformeerd of afgehandeld, wat propagatie bewuster maar ook woordrijker maakt.
De keuze van de benadering hangt af van de architectuur van de applicatie en de taal. In Swift domineert automatische propagatie via throws, in Kotlin — een mix van uitzonderingen (voor onverwachte fouten) en Result-achtige containers (voor verwachte fouten). Het is belangrijk te begrijpen: propagatie is geen doel, maar een noodzaak. Ideale architectuur minimaliseert de diepte van propagatie door fouten af te handelen op het laagst mogelijke niveau waar voldoende context is om een beslissing te nemen.
In Swift vindt propagatie via throws automatisch plaats: als functie A met throws functie B met throws aanroept en A de fout van B niet afhandelt in do-catch, wordt de fout automatisch doorgegeven aan de aanroepende partij van A. Dit elimineert de boilerplate-code die kenmerkend is voor Java checked exceptions, waarbij throws in elke methode van de keten moet worden gedeclareerd. Swift gebruikt het principe „één throws-functie in de keten = de hele keten wordt throws, als er niet wordt afgehandeld op tussenliggende niveaus”.
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 — uiteindelijke afhandelaar
func onButtonTap() {
let vm = UserViewModel()
do {
let user = try vm.loadUser(id: 42)
updateUI(user)
} catch {
showError("Gebruiker laden mislukt")
}
}
Propagatieketen: networkService.request -> fetchUser -> loadUser -> onButtonTap. Elke tussenliggende functie is gemarkeerd met throws en bevat geen do-catch — de fout wordt automatisch omhoog doorgegeven. ViewController onButtonTap is de uiteindelijke afhandelaar met do-catch. Als ViewModel had besloten de fout te transformeren (in een ander type te verpakken), had het do-catch en een nieuwe throw kunnen gebruiken. Automatische propagatie verkort de code: Repository hoeft niet te weten hoe de fout moet worden afgehandeld — dat is de verantwoordelijkheid van ViewController, die toegang heeft tot de UI om een bericht aan de gebruiker te tonen.
In Kotlin vereist propagatie via uitzonderingen geen throws-declaratie in de handtekening (alle uitzonderingen zijn unchecked). De uitzondering stijgt automatisch op de stack totdat deze een try-catch tegenkomt. De afwezigheid van throws in de handtekening maakt propagatie echter impliciet: de ontwikkelaar ziet uit de functiehandtekening niet dat deze een uitzondering kan gooien. Dit is zowel een voordeel (minder boilerplate) als een nadeel (makkelijker om te vergeten af te handelen). Kotlin lost dit probleem op via conventies en architectuurpatronen, niet via de taal.
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")
}
}
}
}
In Repository propagatie met transformatie: bij IOException (netwerk niet beschikbaar) probeert de functie gegevens uit de cache in de database te halen. Als de cache leeg is, gooit deze AppException — propagatie gaat verder met een nieuw fouttype. ViewModel vangt AppException op en vertaalt dit naar UiState.Error — de fout gaat niet verder, propagatie eindigt op het UI-laagniveau. Kotlin Coroutines voegen bijzonderheden toe: uitzonderingen in launch worden automatisch verspreid via CoroutineExceptionHandler, en in async — alleen bij aanroep van await(). Het is belangrijk hiermee rekening te houden bij het ontwerpen van propagatie in coroutines — SupervisorJob voorkomt annulering van de bovenliggende coroutine bij een fout in een kind-coroutine.
Een alternatief voor uitzonderingen — propagatie via een type-container die succes of fout als waarde doorgeeft. In deze benadering retourneert de functie geen waarde, maar een verpakking: Result<T, E> in Swift, Result<T> in Kotlin, Either<L, R> in Dart (uit de pakketten fpdart of dartz). De fout ontrolt de stack niet — hij ligt gewoon in de container en het volgende niveau beslist wat ermee te doen. Dit maakt propagatie explicieter en beheersbaarder.
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> — een eenvoudige container met velden data en error. Sealed class AppError definieert fouttypen (Network, Auth). De functie fetchUser retourneert HttpResult, propagatie vereist geen stack-ontrolling — de aanroepende partij controleert gewoon isSuccess/isError. Deze benadering is bijzonder nuttig in Clean Architecture, waar elke laag (data, domain, presentation) de fout kan transformeren: IOError -> DomainError -> UiError. Propagatie via een container maakt deze transformaties expliciet en testbaar, in tegenstelling tot uitzonderingen waar de transformatieketen niet zichtbaar is in functiehandtekeningen.
Een van de belangrijkste beslissingen bij het ontwerpen van foutafhandeling is de keuze tussen propagatie (doorgeven naar boven) en afhandeling (hier verwerken). De beslissingsregel: handel de fout af op het niveau waar voldoende context is voor een zinvolle actie. Als u toegang hebt tot de UI — toon dan een bericht aan de gebruiker. Als u toegang hebt tot de cache — probeer dan te herstellen. Als u geen van beide hebt — propageer dan.
| Scenario | Actie | Motivatie |
|---|---|---|
| Netwerkfout in Repository | Propageer | Repository weet niet of de gebruiker het verzoek wil herhalen |
| Parsefout in Repository | Handel af (retourneer standaardwaarde) | Repository kent het formaat, kan een fallback-waarde retourneren |
| Timeout in ViewModel | Handel af (UiState.Error) | ViewModel beheert UiState, weet hoe de fout te vertalen |
| Autorisatiefout in Interceptor | Handel af (token vernieuwen) | Interceptor heeft toegang tot tokens en kan de sessie herstellen |
| Onbekende fout in UseCase | Propageer | UseCase heeft geen UI-context — alleen bedrijfslogica |
Gouden regel: minimale propagatie, maximale afhandeling op lagere niveaus. Als Repository uit de cache kan herstellen — moet het dat doen, zonder de fout door te geven. Als ViewModel een Snackbar kan tonen — laat het die tonen, zonder extra code van ViewController te vereisen. Elk propagatieniveau verhoogt de koppeling en bemoeilijkt het testen. Volgens Google Android Architecture Guide (2026) wordt aanbevolen propagatie over laaggrenzen te minimaliseren door sealed class UiState te gebruiken om alle mogelijke toestanden (Loading, Success, Error) op ViewModel-niveau weer te geven en geen uitzonderingen rechtstreeks naar de UI-laag door te geven.
Onjuiste propagatie is een bron van moeilijk te vinden bugs in mobiele applicaties. Laten we vijf belangrijke problemen bekijken waar ontwikkelaars mee worden geconfronteerd en manieren om ze op te lossen.
Het meest voorkomende probleem: tijdens propagatie wordt de uitzondering opgevangen, gelogd en wordt een nieuwe gegooid zonder de originele uitzondering. De ontwikkelaar verliest de StackTrace en kan niet begrijpen waar de fout precies is opgetreden. Gebruik in Swift error chaining: throw MyError(context: originalError). In Kotlin: throw AppException(cause = originalException). In Dart: throw AppException(message, originalException). Maak nooit een nieuwe uitzondering zonder de oorzaak/onderliggende fout door te geven.
catch (e: Exception) { /* niets */ } — een antipatroon dat ertoe leidt dat de applicatie blijft werken in een onjuiste toestand. Als u zeker weet dat de fout kan worden genegeerd — voeg dan een opmerking met motivatie toe. Gebruik in Swift voor optioneel negeren try? (fout -> nil). In Kotlin — Result<T>.onFailure { /* log */ }. Demp uitzonderingen niet zonder loggen.
Als een fout door 5+ niveaus gaat zonder afhandeling, moet de architectuur worden herzien. Elk propagatieniveau is een afhankelijkheid van de throws-handtekening van onderliggende functies. Oplossing: gebruik Failure-containers (sealed class Result { Success, Error }) op laaggrenzen om propagatie expliciet en beperkt te maken. Hoe korter de propagatieketen, hoe gemakkelijker het is om code te testen en te debuggen.
In Kotlin Coroutines annuleert een uitzondering in launch standaard de bovenliggende coroutine en alle siblings (kinderen van dezelfde scope). Als een van 10 parallelle taken faalt, worden de andere 9 geannuleerd, wat vaak ongewenst is. Gebruik SupervisorJob of supervisorScope voor isolatie van fouten: een fout in één child annuleert siblings niet. ViewModelScope gebruikt standaard SupervisorJob, wat beschermt tegen dit probleem in Android.
In een callback-gebaseerde API wordt de fout vaak doorgegeven als parameter van de callback. Als de callback de fout niet afhandelt (of onjuist afhandelt), wordt propagatie impliciet en gaat gemakkelijk verloren. Oplossing: migreer naar async/await (Swift) of coroutines (Kotlin), waar propagatie werkt via standaard try-catch-mechanismen. Als een callback onvermijdelijk is — gebruik dan Either<Error, T> of Result<T> voor gedwongen afhandeling van beide gevallen.
Veelgestelde vragen
Throw is een eenmalige actie van het gooien van een uitzondering. Foutpropagatie is het hele proces van het doorgeven van een fout door meerdere stackniveaus, van throw tot catch. Propagatie omvat throw, automatische of handmatige overdracht via tussenliggende functies en uiteindelijke afhandeling. Het is een breder concept dat de levenscyclus van een fout beschrijft.
Gebruik mock-objecten die uitzonderingen gooien in gegeven scenario's. Controleer of de functie de fout correct propageert of afhandelt via assertThrows (Kotlin/JUnit) of XCTAssertThrowsError (Swift/XCTest). Voor Result-gebaseerde propagatie controleert u isSuccess/isError en waarden in beide gevallen.
Result-propagatie heeft de voorkeur voor verwachte fouten (ongeldige gegevens, bedrijfsregels) binnen één architectuurgrens. Uitzonderingen zijn beter voor onverwachte fouten (netwerkverlies, I/O-fouten) die op hoog niveau moeten worden afgehandeld. Een resultaat met fout onderbreekt de uitvoeringsstroom niet, een uitzondering onderbreekt deze wel.
In Kotlin Coroutines propageert een uitzondering in launch automatisch via CoroutineScope met annulering van siblings. Gebruik supervisorScope of SupervisorJob voor isolatie: een fout in de ene coroutine annuleert andere niet. Voor async moet de fout expliciet worden afgehandeld via try-catch bij aanroep van await(), anders wordt deze doorgeslikt.
De ideale uiteindelijke afhandelaar is de UI-laag (ViewController, Fragment/Composable). Alleen deze heeft toegang tot de gebruikersinterface en kan een bericht, Snackbar of dialoog tonen. Tussenliggende lagen (Repository, UseCase, ViewModel) propageren de fout, waarbij ze deze indien nodig transformeren naar een abstracter domeintype.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook