MWCodebymw.de ↗
Kotlin

Fehler in Coroutinen – coroutineScope, supervisorScope und wer wen mitreißt

Eine von fünf parallelen Abfragen scheitert – und plötzlich sind alle abgebrochen. Das ist kein Bug, sondern strukturierte Nebenläufigkeit. Wer den Unterschied zwischen coroutineScope und supervisorScope kennt, steuert das bewusst.

Fünf Kacheln, fünf Abfragen, eine davon liefert einen Fehler – und der Bildschirm bleibt komplett leer. Der Grund steckt in Kotlins strukturierter Nebenläufigkeit: Scheitert ein Kind, bricht es standardmäßig seinen Eltern-Job ab, und der wiederum alle Geschwister.

Meistens ist das genau richtig. Manchmal ist es das Gegenteil von dem, was du willst.

coroutineScope: alles oder nichts

suspend fun ladeSeite(): Seite = coroutineScope {
    val nutzer   = async { api.nutzer() }
    val einstell = async { api.einstellungen() }
    Seite(nutzer.await(), einstell.await())      // ohne beides gibt es keine Seite
}

Scheitert eine der beiden Abfragen, wird die andere abgebrochen und ladeSeite() wirft. Für Daten, die zusammengehören, ist das die richtige Semantik – niemand baut eine halbe Seite.

supervisorScope: unabhängige Aufgaben

suspend fun ladeDashboard(): Dashboard = supervisorScope {
    val wetter  = async { runCatching { api.wetter() }.getOrNull() }
    val termine = async { runCatching { api.termine() }.getOrNull() }
    val umsatz  = async { runCatching { api.umsatz() }.getOrNull() }
    Dashboard(wetter.await(), termine.await(), umsatz.await())
}

Im supervisorScope reißt ein gescheitertes Kind die Geschwister nicht mit. Zusammen mit runCatching je Zweig bekommst du Teilergebnisse – und kannst in der Oberfläche sagen, welche Kachel gerade nicht kann.

Der wichtige Unterschied bei async

Eine Ausnahme in async wird erst beim await() geworfen. Fängst du sie außen um den Scope herum, kommt sie trotzdem an – aber im coroutineScope sind die Geschwister zu diesem Zeitpunkt schon abgebrochen. Willst du das nicht, muss der try/catch innen sitzen, direkt im jeweiligen async.

// Innen fangen = die anderen laufen weiter
val termine = async {
    try { api.termine() } catch (e: IOException) { emptyList() }
}

CoroutineExceptionHandler – nur für launch, nur oben

private val handler = CoroutineExceptionHandler { _, e ->
    log.error("Unbehandelt im ViewModel", e)
    _stand.value = Zustand.Fehler("Da ist etwas schiefgegangen.")
}

viewModelScope.launch(handler) { … }

Zwei Regeln, die oft übersehen werden: Der Handler greift nur bei launch (nicht bei async, dort gehört der Fehler zum Ergebnis), und er wirkt nur am obersten Job – ein Handler an einem inneren launch wird ignoriert.

Abbruch ist kein Fehler

try {
    api.hole()
} catch (e: CancellationException) {
    throw e                      // MUSS weitergereicht werden
} catch (e: Exception) {
    zeigeFehler(e)
}

Ein pauschales catch (e: Exception) fängt auch die CancellationException, mit der Coroutinen beendet werden – und macht den Abbruch damit kaputt. Entweder gezielt fangen oder die Ausnahme wie oben durchreichen.

Fallstrick

supervisorScope gilt nur für die direkten Kinder. Ein launch innerhalb eines Kindes bildet wieder einen normalen Eltern-Kind-Verbund mit der üblichen Abbruchregel. Und: Der Supervisor macht Fehler nicht harmlos – er verhindert nur die Ausbreitung. Jedes Kind braucht weiterhin eine eigene Behandlung, sonst landet der Fehler unbemerkt im Handler oder im Log.

Nebenläufigkeit, die auch bei wackliger Verbindung noch verständlich reagiert, ist Handwerk. Melde dich über bymw.de.

Quellen

#Kotlin#Coroutinen#Fehlerbehandlung#supervisorScope#Nebenläufigkeit

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 →