Fehler in Flows abfangen – catch, retry und ein sinnvoller Fallback
Ein try/catch um einen Flow herum fängt nichts. Fehler laufen im Flow selbst mit und werden dort behandelt – mit catch für die Ausgabe und retry für den zweiten Versuch bei Netzproblemen.
Das hier funktioniert nicht, und es ist der häufigste Anfängerfehler mit Flows:
// ⚠️ fängt den Fehler NICHT ab
try {
repo.beitraege().collect { liste -> _state.value = liste }
} catch (e: Exception) {
// hier landet man nur zufällig
}Der Grund: Ein Flow ist eine Kette. Ein Fehler entsteht irgendwo weiter oben in dieser Kette — beim Netzwerkaufruf zum Beispiel — und wandert durch alle Zwischenschritte nach unten. Behandeln sollte man ihn dort, wo man weiß, was er bedeutet, und nicht ganz außen, wo alles gleich aussieht.
catch – der Fehler wird zu einem Wert
val ui: StateFlow<UiState> = repo.beitraege()
.map<List<Beitrag>, UiState> { UiState.Erfolg(it) }
.onStart { emit(UiState.Laedt) }
.catch { e ->
emit(UiState.Fehler(e.meldung())) // statt Absturz: ein Zustand
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), UiState.Laedt)catch fängt nur Fehler, die oberhalb entstanden sind — alles, was danach kommt, bleibt unangetastet. Das klingt nach einem Detail, ist aber genau die Eigenschaft, die man will: Ein Fehler in deiner UI-Logik soll nicht so aussehen, als wäre das Netzwerk schuld.
Deshalb gilt: catch gehört ans Ende der Kette, aber vor die Stelle, an der du sammelst.
retry – nochmal, aber nicht unendlich
Bei Netzwerkaufrufen ist der zweite Versuch oft schon erfolgreich. Dafür gibt es retryWhen:
repo.beitraege()
.retryWhen { ursache, versuch ->
val lohntSich = ursache is IOException && versuch < 3
if (lohntSich) delay(1000L * (versuch + 1)) // 1s, 2s, 3s
lohntSich // true = nochmal
}
.catch { emit(UiState.Fehler("Keine Verbindung.")) }Zwei Regeln, die ich mir angewöhnt habe:
- Nur wiederholen, was sich wiederholen lässt. Ein
IOExceptionja. Ein HTTP 401 („nicht eingeloggt") nein — der wird beim zehnten Versuch auch nicht besser, kostet aber Akku und Datenvolumen. - Immer mit wachsender Pause. Drei Versuche direkt hintereinander sind praktisch ein Versuch, weil das Netz in 30 Millisekunden nicht zurückkommt.
Und was ist mit „egal, Hauptsache es stürzt nicht ab"?
Ein leerer catch { } ist verlockend und fast immer falsch. Der Nutzer sieht dann eine leere Liste und denkt, es gäbe nichts — dabei ist nur die Verbindung weg. Mindestens ein unterscheidbarer Zustand sollte drin sein:
sealed interface UiState {
data object Laedt : UiState
data class Erfolg(val daten: List<Beitrag>) : UiState
data class Fehler(val text: String) : UiState
}„Keine Beiträge vorhanden" und „Ich komme gerade nicht ins Netz" sind für den Nutzer zwei völlig verschiedene Nachrichten. Der Unterschied kostet dich zehn Zeilen und entscheidet darüber, ob die App als kaputt oder als ehrlich wahrgenommen wird.
Du hast eine App, die bei schlechtem Netz komisch reagiert? Genau hier liegt es erstaunlich oft. Schreib mir, wenn du dabei ein zweites Paar Augen brauchst.
Quellen
Du brauchst mehr als ein Snippet?
Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.
Projekt anfragen →Verwandte Snippets
callbackFlow – Listener-APIs in einen Flow verwandeln
Android steckt voller Callback-APIs mit `register`/`unregister`. Mit `callbackFlow` machst du daraus einen ganz normalen Flow – inklusive automatischem Abmelden, wenn niemand mehr zuhört.
WorkManager – Arbeit, die auch läuft, wenn die App zu ist
Daten hochladen, nachts synchronisieren, eine Erinnerung schicken: Sobald etwas laufen soll, während die App geschlossen ist, ist WorkManager das richtige Werkzeug – und nicht ein Thread, den das System sowieso abräumt.
rememberUpdatedState in Compose – wenn dein Callback im Effekt einfriert
Ein LaunchedEffect mit key1 = Unit startet genau einmal – und hält damit für immer die Lambda-Referenz vom ersten Durchlauf fest. Klickt der Nutzer später etwas anderes an, passiert das Falsche. rememberUpdatedState löst genau das.