Android Coden
Android 4 min lesen

Error Boundaries: Fehler abfangen, loggen und sinnvoll anzeigen

Lerne, wo du Fehler in Android-Apps gezielt abfängst und transformierst. Klare Grenzen verbessern UX und Stabilität.

Fehler passieren – in jedem Netzwerk, in jeder Datenbank, in jedem Gerätezustand. Die entscheidende Frage ist nicht, ob etwas schiefgeht, sondern wo du es abfängst, wie du es transformierst und was der Nutzer zu sehen bekommt. Error Boundaries sind kein einzelnes Android-API, sondern ein Architekturprinzip: Du legst bewusst fest, an welcher Schicht eines Datenflusses ein Fehler behandelt wird – und was danach passiert.

Was ist das?

Eine Error Boundary ist eine konzeptionelle Grenze in deiner App-Architektur, an der ein Fehler aufgehalten, bewertet und in eine sinnvolle Reaktion umgewandelt wird. Der Begriff stammt ursprünglich aus dem React-Ökosystem, hat sich aber als allgemeines Muster etabliert. In Android-Projekten beschreibt er die Frage: Wer ist dafür zuständig, diesen Fehler zu kennen, und wer darf ihn nach oben durchreichen?

In einer modernen Android-App nach dem offiziellen Architektur-Guide (MVVM mit Repository-Pattern) gibt es typischerweise drei potenzielle Grenzen:

  • Repository-Schicht: Netzwerk- und Datenbankfehler werden in ein einheitliches Result-Objekt verpackt, bevor sie das ViewModel erreichen.
  • ViewModel: Empfängt das Result, entscheidet über Retry-Logik und übersetzt den Fehler in einen UI-State.
  • UI-Schicht (Composable oder Fragment): Zeigt den Fehlerzustand reaktiv an und stellt dem Nutzer ggf. eine Retry-Aktion bereit.

Die kluge Zuweisung dieser Verantwortlichkeiten ist der Kern jeder stabilen Error-Boundary-Strategie.

Wie funktioniert es?

Das Rückgrat ist eine sealed class für den UI-Zustand, die explizit einen Fehlerzustand modelliert:

sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(
        val message: String,
        val retryable: Boolean = true
    ) : UiState<Nothing>()
}

Das Repository kapselt Ausnahmen mit runCatching und gibt ein Result zurück:

suspend fun fetchItems(): Result<List<Item>> = runCatching {
    api.getItems()
}

Das ViewModel abonniert das Repository und übersetzt das Ergebnis in einen UI-State. Beachte das retryable-Flag: Eine IOException signalisiert ein vorübergehendes Problem, das sich wiederholen lässt. Eine IllegalStateException deutet auf einen Logikfehler hin, bei dem ein Retry dem Nutzer nicht hilft.

class ItemViewModel(private val repo: ItemRepository) : ViewModel() {

    private val _state = MutableStateFlow<UiState<List<Item>>>(UiState.Loading)
    val state: StateFlow<UiState<List<Item>>> = _state.asStateFlow()

    fun load() {
        viewModelScope.launch {
            _state.value = UiState.Loading
            repo.fetchItems()
                .onSuccess { _state.value = UiState.Success(it) }
                .onFailure { e ->
                    Log.e("ItemViewModel", "Laden fehlgeschlagen", e)
                    _state.value = UiState.Error(
                        message = e.localizedMessage ?: "Unbekannter Fehler",
                        retryable = e is IOException
                    )
                }
        }
    }
}

Der Log.e-Aufruf ist nicht optional. Er ist der erste Baustein für Observability: Ohne ihn existiert der Fehler nur im Gerät des Nutzers, nicht in deinem Monitoring.

In der Praxis

Composable mit reaktivem Error-State

In Jetpack Compose ist die Umsetzung direkt: Du sammelst den StateFlow und reagierst auf jeden Zustand separat.

@Composable
fun ItemScreen(viewModel: ItemViewModel = hiltViewModel()) {
    val state by viewModel.state.collectAsStateWithLifecycle()

    when (val s = state) {
        is UiState.Loading -> CircularProgressIndicator()
        is UiState.Success -> ItemList(s.data)
        is UiState.Error   -> ErrorView(
            message = s.message,
            onRetry = if (s.retryable) viewModel::load else null
        )
    }
}

Das when-Statement macht den Zustandsraum exhaustiv – der Kotlin-Compiler meldet einen Fehler, falls du einen neuen UiState-Fall vergisst. Das ist kein Zufall, sondern ein bewusstes Architekturmerkmal.

Typische Stolperfalle: Fehler still schlucken

Der häufigste Fehler beim Aufbau von Error Boundaries ist das stille Verwerfen von Exceptions:

// Schlecht – der Fehler verschwindet spurlos
val items = runCatching { api.getItems() }.getOrNull() ?: emptyList()

Der Nutzer sieht eine leere Liste, ohne zu verstehen warum. Der Fehler taucht in keinem Bugtracker auf, weil er nie geloggt wurde. Support-Tickets häufen sich, weil „die App nichts anzeigt”. Logg immer, auch wenn du einen Fallback nutzt, und entscheide danach bewusst, ob der Fehler dem Nutzer sichtbar gemacht werden soll.

Strukturiertes Logging und Crash-Reporting

Eine Error Boundary ist auch der richtige Ort für Crash-Reporting. Firebase Crashlytics erlaubt das Aufzeichnen nicht-fataler Fehler, damit dein Team die Häufigkeit im Dashboard sieht – lange bevor Nutzer die App schlecht bewerten:

.onFailure { e ->
    FirebaseCrashlytics.getInstance().recordException(e)
    Log.e(TAG, "Laden fehlgeschlagen", e)
    _state.value = UiState.Error(
        message = e.localizedMessage ?: "Fehler",
        retryable = e is IOException
    )
}

Kombiniert mit Continuous Integration und automatisierten Tests – wie in den offiziellen Android-CI-Empfehlungen beschrieben – ergibt sich ein vollständiges Sicherheitsnetz: Unit-Tests prüfen die Fehlerlogik im ViewModel, Crashlytics meldet unbehandelte Fälle aus der Produktion.

Fazit

Error Boundaries sind kein nachträglicher Gedanke, sondern ein bewusstes Architekturmerkmal. Indem du klar definierst, wo Fehler abgefangen, geloggt und in UI-Zustände transformiert werden, vermeidest du stille Abstürze, leere Screens ohne Erklärung und fehlende Retry-Optionen. Nimm dir nach dem Lesen dieses Artikels fünf Minuten und gehe deinen eigenen Code durch: Gibt es ViewModels, die Exceptions mit einem leeren catch-Block verschlucken? Gibt es Screens, die bei einem Fehler einfach nichts anzeigen? Schreib für mindestens einen dieser Fälle einen Unit-Test, der einen Fehlerpfad simuliert – du wirst überrascht sein, welche Lücken dabei sichtbar werden.

Quellen (5)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.