Fehlerbehandlung in Dart: try-catch und asynchrone Abläufe
Zuletzt aktualisiert:
8 Min. Lesezeit

Fehlerbehandlung und asynchroner Code sind die Themen, bei denen Lernende zum ersten Mal den Unterschied zwischen „es läuft“ und „es läuft zuverlässig“ spüren. Die zwei Gewohnheiten, die ich im Unterricht am häufigsten sehe: alles mit catch (e) abfangen und den Fehler still verschlucken, und bei einer async-Funktion das await vergessen und dann nicht verstehen, warum der Fehler nie abgefangen wurde. Dieser Beitrag behandelt beides von Grund auf.
Nach den Collections ist das der letzte große Baustein der Dart-Grundlagen. Die Beispiele sind für Dart 3 geschrieben.
Live-Demo: try-catch und async/await
Lernen Sie Fehlerbehandlung und asynchrone Programmierung interaktiv:
try-catch: Fehler abfangen
Programme verhalten sich nicht immer wie erwartet: Der Nutzer tippt etwas Falsches ein, eine Datei fehlt, die Verbindung bricht ab. Sehen wir uns dieses Programm an, das eine Zahl von der Konsole liest:
import 'dart:io';
void main() {
print('Geben Sie eine Zahl ein:');
final eingabe = stdin.readLineSync() ?? '';
final zahl = int.parse(eingabe); // FormatException bei Eingabe von "abc"
print('Ihre Zahl: $zahl');
}Tippt der Nutzer „abc“, wirft int.parse eine FormatException, und das Programm bricht ab. Dieses Verhalten kennen wir aus dem Beitrag über Typumwandlungen. (stdin funktioniert nur in Konsolen-Apps; um das in DartPad zu testen, schreiben Sie die Eingabe in eine Variable.)
Gezielt abfangen mit on
void main() {
const eingabe = 'abc';
try {
final zahl = int.parse(eingabe);
print('Ihre Zahl: $zahl');
} on FormatException catch (e) {
print('Bitte geben Sie eine gültige Zahl ein. (${e.message})');
}
}on FormatException fängt nur Fehler dieses Typs ab. Das nackte catch (e) aus älteren Beispielen fängt dagegen alles, was geworfen wird, auch Ihre Programmierfehler. Auch der Leitfaden Effective Dart rät von catch-Klauseln ohne on ab. Fangen Sie den erwarteten Fehler beim Namen; unerwartete Fehler sollen sichtbar bleiben, damit Sie sie beheben können.
Mehrere Fehlertypen lassen sich getrennt behandeln. Die Reihenfolge zählt: der spezielle zuerst, der allgemeine zuletzt:
void verarbeiten(String eingabe) {
try {
final zahl = int.parse(eingabe);
if (zahl < 0) throw ArgumentError.value(zahl, 'eingabe', 'Darf nicht negativ sein');
print('Ergebnis: ${100 ~/ zahl}');
} on FormatException {
print('Keine Zahl.');
} on ArgumentError catch (e) {
print('Ungültiger Wert: ${e.message}');
} catch (e, stackTrace) {
print('Unerwarteter Fehler: $e');
print(stackTrace); // Zeigt, wo der Fehler entstanden ist
rethrow; // Reicht den Fehler weiter, statt ihn zu verschlucken
}
}Achten Sie hier auf drei Details:
catch (e, stackTrace)liefert als zweiten Parameter den Stacktrace. Beim Debuggen ist er so wertvoll wie der Fehler selbst.rethrowwirft den abgefangenen Fehler samt ursprünglichem Stacktrace erneut. Das nutzen Sie, wenn Sie den Fehler protokollieren und einer höheren Schicht überlassen wollen.throw everliert dagegen den Stacktrace.ArgumentErrorist einError. In Dart steht eineExceptionfür eine erwartbare Situation zur Laufzeit (ungültige Eingabe, Netzwerkfehler), einErrorfür einen Programmierfehler (falsches Argument, Zugriff auf null). Als Grundregel korrigieren Sie beiErrors den Code, statt sie abzufangen und weiterzumachen; dason ArgumentErrorhier dient nur dazu, beide Arten nebeneinander zu zeigen.
Nachfragen, bis die Eingabe gültig ist
import 'dart:io';
void main() {
int? zahl;
while (zahl == null) {
print('Geben Sie eine Zahl ein:');
zahl = int.tryParse(stdin.readLineSync() ?? '');
if (zahl == null) print('Bitte geben Sie eine gültige Zahl ein!');
}
print('Sehr gut! Ihre Zahl: $zahl');
}Die frühere Fassung dieses Beispiels nutzte try-catch in der Schleife. Da ungültige Eingaben hier kein Ausnahmefall, sondern erwartbar sind, passt tryParse besser: Eine null-Prüfung genügt, statt einen Fehler zu werfen und abzufangen.
Der finally-Block
Ein finally-Block läuft, ob ein Fehler auftritt oder nicht:
void main() {
try {
print('Verbindung geöffnet');
int.parse('abc'); // FormatException
} on FormatException catch (e) {
print('Fehler: ${e.message}');
} finally {
print('Verbindung geschlossen'); // Läuft in jedem Fall
}
}Er eignet sich für alles, was in jedem Fall geschlossen werden muss, etwa eine Datei, eine Datenbankverbindung oder eine Ladeanzeige.
Eine eigene Exception-Klasse
Für app-spezifische Fehler können Sie eine Klasse schreiben, die Exception implementiert. Der aufrufende Code kann sie dann mit on beim Namen abfangen:
class ZuWenigGuthaben implements Exception {
final double fehlbetrag;
const ZuWenigGuthaben(this.fehlbetrag);
@override
String toString() => 'ZuWenigGuthaben: $fehlbetrag € fehlen';
}
double bezahlen(double guthaben, double betrag) {
if (betrag > guthaben) throw ZuWenigGuthaben(betrag - guthaben);
return guthaben - betrag;
}
void main() {
try {
bezahlen(100, 150);
} on ZuWenigGuthaben catch (e) {
print('Zahlung fehlgeschlagen, es fehlen ${e.fehlbetrag} €.'); // ... es fehlen 50.0 €.
}
}Einen String zu werfen, etwa throw 'Etwas ist schiefgelaufen', kompiliert zwar, aber die abfangende Seite hat dann keine Typinformation. Werfen Sie eine aussagekräftige Klasse.
Asynchrone Abläufe: async und await
Manche Vorgänge brauchen Zeit: Daten aus dem Netz holen, eine Datei lesen, in eine Datenbank schreiben. Währenddessen soll die App nicht einfrieren.
Räumen wir gleich mit einem verbreiteten Missverständnis auf: Asynchroner Code läuft in Dart nicht in einem eigenen Thread. Dart arbeitet mit einer einzigen Ereignisschleife (Event Loop). await pausiert nur die jeweilige Funktion und lässt die Ereignisschleife andere Arbeit erledigen, bis die erwartete Aufgabe (etwa eine Netzwerkantwort) eintrifft. Findet die erwartete Arbeit außerhalb Ihres Codes statt, wie bei Netzwerk- oder Dateizugriffen, bleibt die Oberfläche dabei flüssig. Eine aufwendige Berechnung in eine async-Funktion zu packen, macht sie aber nicht schneller und friert die Oberfläche trotzdem ein; für solche Arbeit verwenden Sie mit Isolate.run ein eigenes Isolate.
Future, async, await
- Future: Steht für einen Vorgang, dessen Wert später bereitsteht. Er endet entweder mit einem Wert oder mit einem Fehler.
- async: Kennzeichnet eine Funktion, in der
awaitverwendet werden darf; die Funktion gibt automatisch einFuturezurück. - await: Pausiert die Funktion, bis das Future abgeschlossen ist, und liefert das Ergebnis.
Future<void> main() async {
print('Warte auf Daten...');
final daten = await ausDatenbankLaden();
print('Daten empfangen: $daten');
}
Future<String> ausDatenbankLaden() async {
// Wir simulieren die Verzögerung einer Datenbank
await Future.delayed(const Duration(seconds: 3));
return 'Benutzerdaten';
}Was passiert ohne await?
Future<void> main() async {
// String daten = ausDatenbankLaden(); // Kompilierfehler: Future<String> ist kein String
final daten = ausDatenbankLaden(); // Kompiliert, aber daten ist ein Future
print(daten); // Instance of 'Future<String>'
}Die erste Zeile kompiliert nicht, denn die Funktion liefert nicht den Wert, sondern das Versprechen, dass der Wert später kommt (ein Future). Die zweite Zeile ist tückischer: Mit final ergibt die Typinferenz Future<String>, der Code kompiliert, und statt der Daten erscheint Instance of 'Future<String>'. Sehen Sie diese Ausgabe, suchen Sie zuerst nach einem fehlenden await.
Ein Beispiel mit Fortschrittsanzeige
Future<void> main() async {
print('1. Warte auf Daten...');
final daten = await ausDatenbankLaden();
print('2. Daten empfangen: $daten');
}
Future<String> ausDatenbankLaden() async {
for (var i = 1; i <= 5; i++) {
await Future.delayed(const Duration(seconds: 1));
print('Lädt... ${i * 20} %');
}
return 'Datenbankbestand';
}Ausgabe:
1. Warte auf Daten...
Lädt... 20 %
Lädt... 40 %
Lädt... 60 %
Lädt... 80 %
Lädt... 100 %
2. Daten empfangen: DatenbankbestandAsynchrone Fehler abfangen
Endet ein mit await erwartetes Future mit einem Fehler, wird der Fehler in der await-Zeile geworfen und lässt sich mit einem normalen try-catch abfangen:
import 'dart:async';
class ServerFehler implements Exception {
final String meldung;
const ServerFehler(this.meldung);
}
Future<String> vonApiLaden() async {
await Future.delayed(const Duration(seconds: 2));
throw const ServerFehler('Keine Verbindung zum Server');
}
Future<void> main() async {
try {
print('Lade Daten von der API...');
final daten = await vonApiLaden().timeout(const Duration(seconds: 5));
print('Daten: $daten');
} on TimeoutException {
print('Der Server hat nicht rechtzeitig geantwortet.');
} on ServerFehler catch (e) {
print('Fehler: ${e.meldung}');
}
}timeout beendet ein Future, das nicht in der angegebenen Zeit fertig wird, mit einer TimeoutException. Ein Timeout bei Netzwerkanfragen verhindert, dass Nutzer ewig auf eine Ladeanzeige starren.
Nicht erwartete Futures und unawaited
Der Fehler eines Futures ohne await wird vom umgebenden try-catch nicht abgefangen. Der try-Block startet das Future und ist sofort fertig; der Fehler entsteht erst später:
Future<void> main() async {
try {
vonApiLaden(); // kein await: der Fehler wird hier NICHT abgefangen
} on ServerFehler {
print('Wird nie erreicht');
}
}Ein solcher Fehler landet beim Handler der App für nicht abgefangene Fehler. Die Lösung ist meist einfach: await ergänzen. Wollen Sie bewusst nicht auf einen Vorgang warten (etwa ein Analyseereignis im Hintergrund senden), machen Sie das mit unawaited aus dart:async ausdrücklich und behandeln den Fehler am Future selbst:
import 'dart:async';
Future<void> ereignisSenden(String ereignis) async {
// Stellen wir uns vor, hier wird ein Analyseereignis im Hintergrund gesendet
}
void buttonGedrueckt() {
unawaited(
ereignisSenden('button').catchError((Object e) => print('Senden fehlgeschlagen: $e')),
);
}unawaited teilt Lesern und dem Analyzer mit: „Hier nicht zu warten, ist Absicht.“ Der Callback von catchError muss einen Wert liefern, der zum Typ des Futures passt; hier ist der Typ void, also genügt print.
Parallel warten
Unabhängige Vorgänge nacheinander mit await abzuwarten, addiert ihre Dauer:
Future<String> benutzerLaden() async {
await Future.delayed(const Duration(seconds: 1));
return 'Ayse';
}
Future<int> mitteilungenZaehlen() async {
await Future.delayed(const Duration(seconds: 1));
return 3;
}
Future<void> main() async {
// Nacheinander: etwa 2 Sekunden
final name = await benutzerLaden();
final anzahl = await mitteilungenZaehlen();
// Parallel: etwa 1 Sekunde
final (name2, anzahl2) = await (benutzerLaden(), mitteilungenZaehlen()).wait;
print('$name $anzahl / $name2 $anzahl2'); // Ayse 3 / Ayse 3
}Dank der Records aus Dart 3 können Sie Futures unterschiedlicher Typen mit (a, b).wait gemeinsam abwarten und das Ergebnis in einer Zeile zerlegen. Schlägt eines der Futures fehl, wirft .wait einen ParallelWaitError. Für viele Futures desselben Typs funktioniert auch das klassische Future.wait([...]), das die Ergebnisse als List liefert.
Asynchroner Code in Flutter
In Flutter ist asynchroner Code allgegenwärtig: API-Aufrufe, lokale Datenbanken, Dateizugriffe, Berechtigungsanfragen. Das häufigste Muster: Daten in initState laden und das Ergebnis per setState anzeigen:
import 'package:flutter/material.dart';
class DatenSeite extends StatefulWidget {
const DatenSeite({super.key});
@override
State<DatenSeite> createState() => _DatenSeiteState();
}
class _DatenSeiteState extends State<DatenSeite> {
String _daten = 'Lädt...';
@override
void initState() {
super.initState();
_datenLaden();
}
Future<void> _datenLaden() async {
try {
// In einer echten App steht hier ein API-Aufruf
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return; // Abbrechen, falls die Seite inzwischen geschlossen wurde
setState(() => _daten = 'Daten erfolgreich geladen!');
} on Exception catch (e) {
if (!mounted) return;
setState(() => _daten = 'Fehler: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text(_daten)),
);
}
}Die Zeile if (!mounted) return; ist eine wichtige Absicherung, die älteren Beispielen fehlte. Verlässt der Nutzer die Seite, bevor die Daten da sind, wird das Widget aus dem Baum entfernt; setState nach einem await ohne Prüfung aufzurufen, führt zu einem Fehler. Fragen Sie nach jedem await, ob das Widget noch auf dem Bildschirm ist.
Das Future.delayed steht hier für eine echte Netzwerkanfrage. Wie Sie mit dem Paket http echte API-Anfragen senden, Statuscodes prüfen und ein Timeout setzen, zeigt Schritt für Schritt der Beitrag über HTTP-Anfragen und REST-APIs.
FutureBuilder
Flutter bietet ein fertiges Widget, das die Oberfläche anhand des Zustands eines Futures zeichnet:
class ProfilSeite extends StatefulWidget {
const ProfilSeite({super.key});
@override
State<ProfilSeite> createState() => _ProfilSeiteState();
}
class _ProfilSeiteState extends State<ProfilSeite> {
late final Future<String> _daten = ausDatenbankLaden(); // Einmal erzeugt
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _daten,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Fehler: ${snapshot.error}');
}
return Text('Daten: ${snapshot.data}');
},
);
}
}Beachten Sie, dass das Future in einem Feld des State entsteht, nicht in build. Mit future: ausDatenbankLaden() würde die Anfrage bei jedem Neuaufbau erneut gesendet. Ausführlich behandle ich das im Beitrag über FutureBuilder; für Daten, die fortlaufend statt einmalig eintreffen, gibt es den Bruder StreamBuilder.
Regeln und häufige Fehler
awaitist nur inasync-Funktionen erlaubt.
// FALSCH
void etwasTun() {
await etwas(); // Kompilierfehler
}
// RICHTIG
Future<void> etwasTun() async {
await etwas();
}Der Rückgabetyp einer async-Funktion sollte ein
Futuresein.void f() asynckompiliert, aber der Aufrufer kann weder auf die Funktion warten noch ihre Fehler abfangen. Für asynchrone Funktionen ohne Rückgabewert schreiben SieFuture<void>.Unabhängige Vorgänge nicht nacheinander abwarten. Starten Sie sie mit dem Record-
.waitvon oben oder mitFuture.waitparallel.Fehler beim Namen abfangen.
on FormatException,on TimeoutException,on ServerFehler; ein nacktescatch (e)gehört nur ganz ans Ende, zum Protokollieren und fürrethrow.Fehler nicht still verschlucken. Ein leerer
catch-Block löst kein Problem, er versteckt es nur.Nach
awaitaufmountedprüfen. In Flutter kann das Widget den Baum inzwischen verlassen haben.
Nächster Schritt
Die Code-Seite von Dart ist geschafft; als Nächstes kommt die Oberfläche: Widget-Baum, Row, Column und Stack in Flutter.
Ressourcen
- Dart: Error handling
- Dart: Asynchronous programming (futures, async, await)
- Effective Dart: Usage (Error handling)
- Flutter: Handling errors
Noch Fragen?
Häufig gestellte Fragen
Was ist der Unterschied zwischen catch (e) und on FormatException catch (e)?
catch (e) fängt alles ab, was geworfen wird; on FormatException catch (e) nur Fehler dieses Typs. Behandeln Sie bestimmte Fehler zuerst einzeln mit on und setzen Sie das allgemeine catch ans Ende, damit Sie unerwartete Fehler nicht versehentlich verschlucken.
Warum wird der Fehler einer async-Funktion nicht abgefangen, wenn ich sie ohne await aufrufe?
Ohne await gibt die Funktion sofort ein Future zurück, und der Fehler entsteht in diesem Future, wenn der try-Block längst beendet ist. Setzen Sie await vor den Aufruf, oder markieren Sie den Aufruf mit unawaited, wenn Sie bewusst nicht warten, und fangen Sie den Fehler mit .catchError am Future ab.
Was ist der Unterschied zwischen throw e und rethrow?
rethrow wirft den abgefangenen Fehler samt ursprünglichem Stacktrace erneut. throw e tut so, als würde der Fehler an einer neuen Stelle geworfen, und die Information, wo er zuerst entstand, geht verloren. Wollen Sie einen Fehler aus einem catch-Block weiterreichen, nehmen Sie rethrow.
Warum sollte ich dem future-Parameter von FutureBuilder keinen direkten Funktionsaufruf übergeben?
build läuft bei jedem Neuaufbau, daher löst future: datenLaden() die Anfrage jedes Mal neu aus. Erzeugen Sie das Future einmal in einem Feld des State (etwa in initState oder mit late final) und übergeben Sie FutureBuilder dieses Feld.
Verwandte Artikel
Flutter: Asynchrone Listen mit FutureBuilder
FutureBuilder in Flutter: Snapshot-Zustände richtig lesen, der Fehler „Future in build erzeugen“, die Lösung mit initState und Aktualisieren per setState.
Flutter: Lokale Datenspeicherung mit SharedPreferences
Theme und Einstellungen mit shared_preferences speichern: SharedPreferencesAsync, WithCache und alte API im Vergleich, Migration und was nicht hineingehört.
Flutter: HTTP-Anfragen und REST-APIs
GET, POST, PUT und DELETE mit dem http-Paket in Flutter, JSON in Modellklassen, Statuscodes, Timeouts sowie Lade- und Fehlerzustände mit FutureBuilder.