Android Coden
Android 7 min lesen

Strategie für Fehlerbehandlung

Du lernst, Fehler gezielt einzuordnen. So trennst du behebbare Probleme von Bugs und Abstürzen.

Eine Error Handling Strategy ist deine bewusste Regel, wie deine Android-App mit Fehlern umgeht. Du entscheidest nicht erst im Einzelfall, ob du einen Toast zeigst, einen Crash akzeptierst oder einen Retry anbietest. Du unterscheidest vorher: Ist das ein erwartbares Problem für den Nutzer, ein temporärer Ausfall oder ein Programmierfehler, der im Code behoben werden muss?

Was ist das?

Eine Strategie für Fehlerbehandlung beschreibt, wie du Fehler erkennst, einordnest, weiterreichst und für den Nutzer sichtbar machst. Im Android-Kontext geht es oft um Netzwerkfehler, ungültige Eingaben, leere Daten, fehlende Berechtigungen, abgelaufene Sessions oder lokale Speicherprobleme. Diese Fälle sind nicht automatisch Bugs. Sie passieren in echten Apps, auch wenn dein Code korrekt ist.

Das zentrale Denkmodell lautet: Behebbare, nutzernahe Fehler sind Teil des App-Zustands. Programmierfehler sind dagegen Defekte im Code. Wenn eine API gerade nicht erreichbar ist, kann deine App eine Meldung und einen erneuten Versuch anbieten. Wenn du aber in Kotlin mit !! einen Wert erzwingst und dadurch eine NullPointerException auslöst, ist das kein normaler UI-Zustand. Das gehört nicht als hübsche Fehlermeldung versteckt, sondern muss im Code, in Tests oder im Review auffallen.

Diese Trennung ist wichtig, weil Android-Apps in vielen unsicheren Umgebungen laufen. Geräte wechseln Netzwerke, Prozesse werden beendet, Eingaben sind unvollständig, Backends antworten langsam, Berechtigungen werden entzogen. Eine robuste App behandelt solche Bedingungen ruhig und nachvollziehbar. Gleichzeitig darf sie echte Programmierfehler nicht verschleiern. Wenn du jeden Fehler abfängst und nur „Etwas ist schiefgelaufen“ anzeigst, machst du Debugging schwerer und senkst die Qualität deiner App.

In modernen Android-Projekten hängt Fehlerbehandlung eng mit Architektur zusammen. Repositorys kennen technische Details, ViewModels übersetzen Ergebnisse in UI-State, und Jetpack Compose rendert diesen Zustand. So bleibt die Oberfläche testbar und reagiert vorhersehbar. Die Strategie ist damit kein einzelner try-catch-Block, sondern eine Vereinbarung zwischen Schichten.

Wie funktioniert es?

Du beginnst mit einer Einordnung. Ein Fehler kann erwartbar und wiederherstellbar sein, etwa ein Timeout beim Laden einer Liste. Ein Fehler kann eine Nutzeraktion verlangen, etwa eine fehlende Internetverbindung oder eine verweigerte Berechtigung. Ein Fehler kann auch ein technischer Defekt sein, etwa ein nicht behandelter Zustand in einer when-Verzweigung. Diese Kategorien brauchen unterschiedliche Reaktionen.

Für wiederherstellbare Fehler brauchst du einen klaren Pfad zurück. Das kann ein Retry-Button sein, ein Formularhinweis, ein Login-Dialog oder ein Fallback auf zwischengespeicherte Daten. Wichtig ist, dass die App dem Nutzer nicht nur sagt, dass etwas fehlgeschlagen ist, sondern was jetzt möglich ist. Gute User Feedback ist konkret, knapp und handlungsorientiert. „Keine Verbindung. Erneut versuchen“ ist hilfreicher als „Fehler 5001“.

Technisch solltest du Fehler nicht ungeordnet durch die App werfen. In Kotlin kannst du dafür eigene Ergebnis-Typen, Result, sealed Klassen oder klar benannte Domain-Fehler nutzen. Entscheidend ist nicht die eine perfekte API, sondern die Konsistenz. Wenn ein Repository mal Exceptions wirft, mal null liefert und mal einen String zurückgibt, wird das ViewModel unnötig kompliziert. Anfänger bauen dadurch oft UI-Code, der zu viel über HTTP-Codes, Datenbankdetails oder interne Exceptions weiß.

Ein ViewModel sollte technische Ergebnisse in UI-Zustände übersetzen. Ein typisches Modell enthält Ladezustand, Daten und Fehlerzustand. Compose liest diesen State und zeigt passende Elemente an. Der Nutzer sieht dann nicht die Exception, sondern eine verständliche Meldung mit einer Aktion. Logs und Crash-Reports können weiterhin technische Details enthalten, aber sie gehören nicht ungefiltert in die Oberfläche.

Du brauchst außerdem eine klare Grenze zwischen Fehlerbehandlung und Fehlervermeidung. Validierung von Eingaben verhindert manche Fehler früh. Tests prüfen, ob erwartete Fehlerpfade funktionieren. Code-Reviews prüfen, ob du echte Bugs nicht als normale Fehler behandelst. Continuous Integration sorgt dafür, dass solche Tests nicht nur lokal laufen, sondern bei Änderungen regelmäßig ausgeführt werden. So wird Fehlerbehandlung Teil deiner Qualitätsroutine.

Ein häufiger Irrtum ist: „Wenn die App nicht crasht, ist die Fehlerbehandlung gut.“ Das stimmt nicht. Eine App kann ohne Crash trotzdem schlecht reagieren: endloser Ladeindikator, doppelte Snackbar, verlorene Eingabe, keine Retry-Möglichkeit oder unverständliche Meldung. Umgekehrt ist ein Crash in der Entwicklungsphase manchmal ein wertvolles Signal, wenn er einen falschen Programmzustand zeigt, den du nicht still akzeptieren solltest.

In der Praxis

Stell dir eine Compose-App vor, die eine Artikelliste lädt. Das Repository ruft eine API auf. Das ViewModel soll drei Dinge sauber abbilden: Laden, Erfolg und behebbare Fehler. Ein möglicher UI-State sieht so aus:

sealed interface ArticleListUiState {
    data object Loading : ArticleListUiState
    data class Content(val articles: List<Article>) : ArticleListUiState
    data class Error(
        val message: String,
        val canRetry: Boolean
    ) : ArticleListUiState
}

class ArticleListViewModel(
    private val repository: ArticleRepository
) : ViewModel() {

    var uiState by mutableStateOf<ArticleListUiState>(ArticleListUiState.Loading)
        private set

    fun loadArticles() {
        viewModelScope.launch {
            uiState = ArticleListUiState.Loading

            uiState = try {
                val articles = repository.loadArticles()
                ArticleListUiState.Content(articles)
            } catch (exception: IOException) {
                ArticleListUiState.Error(
                    message = "Die Artikel konnten nicht geladen werden.",
                    canRetry = true
                )
            }
        }
    }
}

Dieses Beispiel ist bewusst klein. Es zeigt aber die wichtigste Regel: Eine erwartbare Störung wird in einen UI-State übersetzt. Die UI muss keine IOException kennen. Sie fragt nur: Gibt es Inhalt, wird geladen oder soll ein Fehler mit Retry angezeigt werden?

In Compose könntest du diesen State dann mit einer when-Verzweigung rendern. Bei Loading zeigst du einen Fortschrittsindikator, bei Content die Liste, bei Error eine Meldung und optional einen Button. Der Retry ruft wieder loadArticles() auf. Damit bekommt der Nutzer einen Wiederherstellungspfad, statt in einer Sackgasse zu landen.

Die Beispielstrategie ist aber noch nicht vollständig. Du solltest nicht jede Exception pauschal fangen. IOException steht hier für ein erwartbares I/O-Problem. Eine IllegalStateException, die durch einen falschen internen Zustand entsteht, sollte nicht automatisch als Netzwerkproblem dargestellt werden. Sonst maskierst du einen Programmierfehler. In größeren Apps definierst du oft eigene Fehlerklassen, etwa NetworkUnavailable, Unauthorized, ServerUnavailable oder ValidationFailed. Diese werden dann in passende UI-Reaktionen übersetzt.

Eine praktische Entscheidungsregel lautet: Wenn der Nutzer durch Warten, Korrigieren, Erneutversuchen oder Anmelden sinnvoll reagieren kann, behandle den Fehler als Teil des UI-Flows. Wenn nur der Entwickler den Zustand korrigieren kann, behandle ihn als Bug. Ein abgelaufener Login ist ein App-Zustand. Ein fehlendes Mapping für einen bekannten Serverwert ist wahrscheinlich ein Programmierfehler. Eine leere Ergebnisliste ist meist kein Fehler, sondern ein eigener Zustand.

Typische Stolperfallen erkennst du schnell in Code-Reviews. Erstens: Fehlermeldungen werden direkt aus Exceptions übernommen. Das führt zu technischen Texten und kann interne Details verraten. Zweitens: Fehler werden zu früh in Strings verwandelt. Dann kannst du später nicht mehr sauber entscheiden, ob ein Retry, Login oder Formularhinweis nötig ist. Drittens: Lade-, Erfolgs- und Fehlerzustand werden über mehrere Booleans verteilt, etwa isLoading, hasError, isEmpty. Diese Kombinationen können widersprüchlich werden. Ein geschlossener UI-State mit klaren Varianten ist meistens robuster.

Auch Tests gehören zur Strategie. Du kannst im Unit-Test prüfen, ob das ViewModel bei einem Repository-Fehler den passenden Error-State setzt. Du kannst testen, dass ein Retry erneut lädt. In UI-Tests kannst du prüfen, ob die Fehlermeldung und der Button sichtbar sind. In der CI laufen diese Tests regelmäßig, damit spätere Änderungen deine Fehlerpfade nicht unbemerkt brechen. Das ist besonders wichtig, weil Entwickler oft den Erfolgsfall manuell ausprobieren, aber Fehlerfälle seltener anklicken.

Für Android-Qualität zählt außerdem, dass Fehlerbehandlung zur Plattform passt. Blockiere nicht den Main Thread, nur um einen Fehler synchron zu behandeln. Verliere bei Konfigurationsänderungen nicht sofort den Zustand. Zeige Fehlermeldungen nicht mehrfach, wenn Compose neu rendert. Nutze State für dauerhafte Fehleranzeigen und Events nur für einmalige Aktionen, etwa eine Snackbar. Auch hier hilft ein klares Modell: Was ist Zustand, was ist Aktion, was ist ein Defekt?

Fazit

Eine gute Error Handling Strategy macht deine App nicht nur stabiler, sondern auch verständlicher. Du trennst erwartbare Ausfälle von Programmierfehlern, gibst dem Nutzer einen klaren nächsten Schritt und hältst technische Details in den passenden Schichten. Prüfe das aktiv: Schreibe für einen Ladeflow mindestens einen Test für den Fehlerfall, simuliere im Debugger eine fehlgeschlagene Repository-Antwort und achte im Code-Review darauf, ob Fehler zu früh verschluckt oder als unklare Strings durchgereicht werden.

Quellen (3)
Redaktion

Geschrieben von

Redaktion

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