Error Propagation ist ein Mechanismus zur Weitergabe eines Fehlers den Aufrufstapel hinauf vom Entstehungsort bis zum Behandler. Wenn eine Funktion einen Fehler nicht selbst behandeln kann, gibt sie ihn an die aufrufende Seite weiter — durch eine Ausnahme (Exception), throws-Deklaration oder Return Type. Die korrekte Implementierung der Propagation ist kritisch für die Stabilität mobiler Anwendungen: Unbehandelte oder falsch weitergegebene Fehler führen zu Abstürzen. Laut Apple Swift Documentation (2026) ermöglicht automatische Propagation durch throws in Swift die Weitergabe eines Fehlers auf jede Ebene ohne Boilerplate-Code.
Wichtigste Punkte
Error Propagation (Fehlerweitergabe) ist der Prozess der Übergabe eines Fehlerobjekts von der Funktion, in der er auftrat, die Aufrufkette hinauf zum nächsten geeigneten Behandler. Stellen Sie sich den Aufrufstapel vor: ViewController ruft ViewModel auf, ViewModel ruft Repository auf, Repository ruft API auf. Wenn die API einen Netzwerkfehler zurückgibt, muss dieser durch Repository und ViewModel zu ViewController gelangen, der dem Benutzer eine Meldung anzeigt. Jede Zwischenfunktion entscheidet: den Fehler behandeln oder weitergeben (propagieren).
Es gibt zwei Ansätze für die Propagation: automatisch und manuell. Beim automatischen Ansatz (Swift throws, Java geprüfte Ausnahmen) zwingt der Compiler den Entwickler, den Fehler entweder zu behandeln oder die Propagation in der Signatur zu deklarieren. Beim manuellen Ansatz (Result Type, Kotlin Try) wird der Fehler als Wert übergeben — der Entwickler schreibt explizit Code zum Weitergeben oder Transformieren des Fehlers. Laut Kotlin Result Docs (2026) ist Result<T> in Kotlin nicht für die direkte Propagation über Funktionsgrenzen hinweg vorgesehen — es muss auf jeder Ebene transformiert oder behandelt werden, was die Propagation bewusster, aber auch wortreicher macht.
Die Wahl des Ansatzes hängt von der Anwendungsarchitektur und der Sprache ab. In Swift dominiert die automatische Propagation durch throws, in Kotlin wird eine Mischung aus Ausnahmen (für unerwartete Fehler) und Result-ähnlichen Containern (für erwartete Fehler) verwendet. Es ist wichtig zu verstehen: Propagation ist kein Ziel, sondern eine Notwendigkeit. Eine ideale Architektur minimiert die Tiefe der Propagation und behandelt Fehler auf der niedrigstmöglichen Ebene, auf der genügend Kontext für eine Entscheidung vorhanden ist.
In Swift ist die Propagation durch throws automatisch: Wenn Funktion A mit throws Funktion B mit throws aufruft und A den Fehler von B nicht in do-catch behandelt, wird der Fehler automatisch an den Aufrufer von A weitergegeben. Dies eliminiert den für Java-geprüfte Ausnahmen typischen Boilerplate-Code, bei dem throws in jeder Methode der Kette deklariert werden muss. Swift verwendet das Prinzip „eine throws-Funktion in der Kette = die gesamte Kette wird zu throws, wenn nicht auf Zwischenebenen behandelt“.
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 — endgültiger Behandler
func onButtonTap() {
let vm = UserViewModel()
do {
let user = try vm.loadUser(id: 42)
updateUI(user)
} catch {
showError("Benutzer konnte nicht geladen werden")
}
}
Propagationskette: networkService.request -> fetchUser -> loadUser -> onButtonTap. Jede Zwischenfunktion ist mit throws markiert und enthält kein do-catch — der Fehler wird automatisch nach oben weitergegeben. ViewController onButtonTap ist der endgültige Behandler mit do-catch. Wenn ViewModel beschließen würde, den Fehler zu transformieren (in einen anderen Typ zu verpacken), könnte es do-catch und einen neuen throw verwenden. Automatische Propagation reduziert Code: Repository muss nicht wissen, wie der Fehler zu behandeln ist — das ist die Verantwortung von ViewController, der Zugriff auf die UI hat, um dem Benutzer eine Meldung anzuzeigen.
In Kotlin erfordert die Propagation durch Ausnahmen keine throws-Deklaration in der Signatur (alle Ausnahmen sind ungeprüft). Die Ausnahme steigt automatisch den Stapel hinauf, bis sie auf ein try-catch trifft. Das Fehlen von throws in der Signatur macht die Propagation jedoch implizit: Der Entwickler kann der Funktionssignatur nicht ansehen, dass sie eine Ausnahme auslösen könnte. Dies ist gleichzeitig ein Plus (weniger Boilerplate) und ein Minus (leichter zu vergessen, sie zu behandeln). Kotlin löst dieses Problem durch Konventionen und Architekturmuster, nicht durch die Sprache selbst.
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")
}
}
}
}
Im Repository, Propagation mit Transformation: Bei IOException (Netzwerk nicht verfügbar) versucht die Funktion, zwischengespeicherte Daten aus der Datenbank abzurufen. Ist der Cache leer, löst sie AppException aus — die Propagation wird mit einem neuen Fehlertyp fortgesetzt. ViewModel fängt AppException ab und übersetzt es in UiState.Error — der Fehler geht nicht weiter, die Propagation endet auf der Ebene der UI-Schicht. Kotlin Coroutines fügen Besonderheiten hinzu: Ausnahmen in launch propagieren automatisch über CoroutineExceptionHandler, in async — nur beim Aufruf von await(). Dies ist wichtig bei der Gestaltung der Propagation in Koroutinen — SupervisorJob verhindert die Abbruch der übergeordneten Koroutine bei einem Fehler in einer untergeordneten Koroutine.
Eine Alternative zu Ausnahmen ist die Propagation durch einen Containertyp, der Erfolg oder Fehler als Wert übergibt. Bei diesem Ansatz gibt die Funktion keinen Wert zurück, sondern einen Wrapper: Result<T, E> in Swift, Result<T> in Kotlin, Either<L, R> in Dart (aus dem Paket fpdart oder dartz). Der Fehler wickelt den Stapel nicht ab — er liegt einfach im Container, und die nächste Ebene entscheidet, was damit zu tun ist. Dies macht die Propagation expliziter und kontrollierbarer.
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> ist ein einfacher Container mit Daten- und Fehlerfeldern. Die versiegelte Klasse AppError definiert Fehlertypen (Network, Auth). Die Funktion fetchUser gibt HttpResult zurück, die Propagation erfordert keine Stapelabwicklung — der Aufrufer prüft einfach isSuccess/isError. Dieser Ansatz ist besonders nützlich in Clean Architecture, wo jede Schicht (data, domain, presentation) den Fehler transformieren kann: IOError -> DomainError -> UiError. Die Propagation durch einen Container macht diese Transformationen explizit und testbar, im Gegensatz zu Ausnahmen, bei denen die Transformationskette in den Funktionssignaturen nicht sichtbar ist.
Eine der wichtigsten Entscheidungen beim Entwurf der Fehlerbehandlung ist die Wahl zwischen Propagation (nach oben weitergeben) und Behandlung (hier behandeln). Die Entscheidungsregel: Behandle den Fehler auf der Ebene, auf der genügend Kontext für eine sinnvolle Aktion vorhanden ist. Wenn du Zugriff auf die UI hast — zeige dem Benutzer eine Meldung. Wenn du Zugriff auf den Cache hast — versuche eine Wiederherstellung. Wenn du weder noch hast — propagiere.
| Szenario | Aktion | Begründung |
|---|---|---|
| Netzwerkfehler im Repository | Propagieren | Repository weiß nicht, ob der Benutzer die Anfrage wiederholen möchte |
| Parse-Fehler im Repository | Behandeln (Standardwert zurückgeben) | Repository kennt das Format, kann einen Fallback-Wert zurückgeben |
| Zeitüberschreitung im ViewModel | Behandeln (UiState.Error) | ViewModel verwaltet UiState, weiß, wie der Fehler zu übersetzen ist |
| Autorisierungsfehler im Interceptor | Behandeln (Token aktualisieren) | Interceptor hat Zugriff auf Token und kann die Sitzung wiederherstellen |
| Unbekannter Fehler im UseCase | Propagieren | UseCase hat keinen UI-Kontext — nur Geschäftslogik |
Die goldene Regel: Minimale Propagation, maximale Behandlung auf den unteren Ebenen. Wenn Repository sich aus dem Cache erholen kann — sollte es dies tun, ohne den Fehler nach oben weiterzugeben. Wenn ViewModel einen Snackbar anzeigen kann — soll es ihn anzeigen, ohne zusätzlichen Code von ViewController zu erfordern. Jede Propagationsebene erhöht die Kopplung und erschwert das Testen. Laut Google Android Architecture Guide (2026) wird empfohlen, die Propagation über Schichtgrenzen hinweg zu minimieren, indem man eine versiegelte Klasse UiState verwendet, um alle möglichen Zustände (Loading, Success, Error) auf ViewModel-Ebene darzustellen, und Ausnahmen nicht direkt an die UI-Schicht weitergibt.
Falsche Propagation ist eine Quelle schwer auffindbarer Fehler in mobilen Anwendungen. Betrachten wir fünf Hauptprobleme, mit denen Entwickler konfrontiert sind, und Möglichkeiten zu deren Lösung.
Das häufigste Problem: Während der Propagation wird eine Ausnahme abgefangen, protokolliert und eine neue ohne die ursprüngliche Ausnahme ausgelöst. Der Entwickler verliert den StackTrace und kann nicht verstehen, wo genau der Fehler aufgetreten ist. Verwenden Sie in Swift Fehlerverkettung: throw MyError(context: originalError). In Kotlin: throw AppException(cause = originalException). In Dart: throw AppException(message, originalException). Erstellen Sie niemals eine neue Ausnahme, ohne cause/underlyingError zu übergeben.
catch (e: Exception) { /* nichts */ } ist ein Antipattern, das dazu führt, dass die Anwendung in einem fehlerhaften Zustand weiterläuft. Wenn Sie sicher sind, dass der Fehler ignoriert werden kann — fügen Sie einen Kommentar mit Begründung hinzu. Verwenden Sie in Swift try? für optionales Ignorieren (Fehler -> nil). In Kotlin — Result<T>.onFailure { /* log */ }. Schlucken Sie keine Ausnahmen ohne Protokollierung.
Wenn ein Fehler 5+ Ebenen ohne Behandlung durchläuft, muss die Architektur überdacht werden. Jede Propagationsebene ist eine Abhängigkeit von der throws-Signatur der darunterliegenden Funktionen. Lösung: Verwenden Sie Failure-Container (sealed class Result { Success, Error }) an Schichtgrenzen, um die Propagation explizit und begrenzt zu machen. Je kürzer die Propagationskette, desto einfacher ist das Testen und Debuggen des Codes.
In Kotlin Coroutines bricht eine Ausnahme in launch standardmäßig die übergeordnete Koroutine und alle Geschwister (Kinder desselben Bereichs) ab. Wenn eine von 10 parallelen Aufgaben fehlschlägt, werden die anderen 9 abgebrochen, was oft unerwünscht ist. Verwenden Sie SupervisorJob oder supervisorScope, um Fehler zu isolieren: Ein Fehler in einem Kind bricht keine Geschwister ab. ViewModelScope verwendet standardmäßig SupervisorJob, was in Android vor diesem Problem schützt.
In Callback-basierten APIs wird der Fehler oft als Parameter des Callbacks übergeben. Wenn der Callback den Fehler nicht behandelt (oder falsch behandelt), wird die Propagation implizit und geht leicht verloren. Lösung: Migrieren Sie zu async/await (Swift) oder Koroutinen (Kotlin), wo die Propagation durch Standard-try-catch-Mechanismen funktioniert. Wenn ein Callback unvermeidbar ist — verwenden Sie Either<Error, T> oder Result<T> zur erzwungenen Behandlung beider Fälle.
Häufig gestellte Fragen
Throw ist eine einmalige Aktion des Auslösens einer Ausnahme. Error Propagation ist der gesamte Prozess der Weitergabe eines Fehlers durch mehrere Stapelebenen, von throw bis catch. Die Propagation umfasst throw, die automatische oder manuelle Weitergabe durch Zwischenfunktionen und die endgültige Behandlung. Es ist ein breiteres Konzept, das den Lebenszyklus eines Fehlers beschreibt.
Verwenden Sie Mock-Objekte, die in bestimmten Szenarien Ausnahmen auslösen. Überprüfen Sie, ob die Funktion den Fehler korrekt propagiert oder behandelt, mit assertThrows (Kotlin/JUnit) oder XCTAssertThrowsError (Swift/XCTest). Für Result-basierte Propagation überprüfen Sie isSuccess/isError und die Werte in beiden Fällen.
Result-Propagation ist vorzuziehen für erwartete Fehler (ungültige Daten, Geschäftsregeln) innerhalb einer Architekturgrenze. Ausnahmen sind besser für unerwartete Fehler (Netzwerkverlust, E/A-Fehler), die auf einer hohen Ebene behandelt werden sollten. Ein Ergebnis mit Fehler unterbricht den Ausführungsfluss nicht, eine Ausnahme schon.
In Kotlin Coroutines propagiert eine Ausnahme in launch automatisch durch CoroutineScope mit Abbruch von Geschwistern. Verwenden Sie supervisorScope oder SupervisorJob zur Isolierung: Ein Fehler in einer Koroutine bricht andere nicht ab. Bei async muss der Fehler explizit durch try-catch beim Aufruf von await() behandelt werden, sonst wird er geschluckt.
Der ideale endgültige Behandler ist die UI-Schicht (ViewController, Fragment/Composable). Nur sie hat Zugriff auf die Benutzeroberfläche und kann eine Meldung, Snackbar oder einen Dialog anzeigen. Zwischenschichten (Repository, UseCase, ViewModel) propagieren den Fehler und transformieren ihn bei Bedarf in einen abstrakteren Domänentyp.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch