Foutpropagatie: wat is het, mechanisme van foutverspreiding en hoe werkt het in mobiele ontwikkeling

Auteur: IT Sectr Gepubliceerd: 2026-05-26 Leestijd: 9 min

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 doorgeven van een fout van de plaats van ontstaan omhoog door de stack naar de afhandelaar, tussenliggende functies overslaand
  • Automatische propagatie via throws in Swift geeft de fout door zonder expliciete code op elk stackniveau
  • Handmatige propagatie in Kotlin en Dart vereist expliciete try-catch of overdracht in een Result-container op elk niveau
  • Checked exceptions in Java dwingen propagatie af via throws in de handtekening, unchecked staan negeren toe
  • Result Type — een alternatief voor uitzonderingen, waarbij de fout als waarde wordt doorgegeven zonder stack-ontrollen

Wat is foutpropagatie?

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.

Propagatie via Throws in Swift

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”.

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 — 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.

Propagatie via uitzonderingen in Kotlin

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.

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

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.

Propagatie via Result Type

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.

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> — 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.

Propagatie vs Afhandeling: wanneer doorgeven, wanneer afhandelen

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.

ScenarioActieMotivatie
Netwerkfout in RepositoryPropageerRepository weet niet of de gebruiker het verzoek wil herhalen
Parsefout in RepositoryHandel af (retourneer standaardwaarde)Repository kent het formaat, kan een fallback-waarde retourneren
Timeout in ViewModelHandel af (UiState.Error)ViewModel beheert UiState, weet hoe de fout te vertalen
Autorisatiefout in InterceptorHandel af (token vernieuwen)Interceptor heeft toegang tot tokens en kan de sessie herstellen
Onbekende fout in UseCasePropageerUseCase 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.

Problemen en antipatronen van foutpropagatie

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.

Verlies van foutcontext

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.

Negeren van de fout (lege catch)

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.

Overmatige propagatiediepte

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.

Propagatie in coroutines zonder SupervisorJob

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.

Propagatie via callback zonder afhandeling

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

Wat is het verschil tussen foutpropagatie en throw?

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.

Hoe test ik foutpropagatie?

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.

Wanneer is propagatie via Result beter dan uitzonderingen?

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.

Hoe propageer ik een fout door Kotlin-coroutines?

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.

Welk niveau moet de uiteindelijke foutafhandelaar zijn?

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

  • Foutpropagatie — het doorgeven van een fout omhoog door de stack van de plaats van ontstaan naar de afhandelaar via uitzonderingen of Result-containers
  • Automatische propagatie in Swift via throws vereist geen code op tussenliggende niveaus — de fout stijgt vanzelf
  • Handmatige propagatie in Kotlin via expliciete try-catch en throw op elk niveau maakt afhandeling bewust maar woordrijk
  • Result Type geeft de fout door als waarde zonder stack-ontrolling, handig voor verwachte fouten in Clean Architecture
  • Afhandelingsregel: handel af op het niveau met context (UI), propageer door niveaus zonder context (domain, data)
  • Antipatronen: lege catch, verlies van oorzaak bij transformatie, overmatige propagatiediepte, negeren van SupervisorJob
  • Ontwerp getypeerde fouten (sealed class / enum) voor elke laag en transformeer ze bij het overschrijden van laaggrenzen

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.

Bespreek het project

Lees ook