Result, Option und der Weg ohne unwrap
Rust kennt keine Ausnahmen, die zehn Ebenen überspringen. Fehler reisen als Rückgabewert, und wer das einmal sauber aufsetzt, bekommt im Betrieb Meldungen, mit denen sich arbeiten lässt.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Unwrap ist schnell geschrieben und kostet im Betrieb am meisten
In den ersten Wochen ist unwrap der Weg, am Compiler vorbeizukommen. Es übersetzt, die Tests laufen durch, und niemand beschwert sich. Im Betrieb wird daraus ein Abbruch, dessen Meldung Datei und Zeile nennt und sonst nichts: nicht, welcher Datensatz, welcher Pfad oder welcher Konfigurationseintrag den Fall ausgelöst hat. Das ist pro Vorfall eine Fehlersuche, die mit einer einzigen Zeile beim Schreiben vermeidbar gewesen wäre.
Der zweite Preis ist struktureller Natur. Wenn jede Funktion einen anderen Fehlertyp zurückgibt und niemand festgelegt hat, wie diese Typen zusammenpassen, hört der Fragezeichen-Operator auf zu funktionieren, und an jeder Aufrufstelle wächst eine Verzweigung. Der übliche Ausweg ist dann ein Text als Fehlertyp, und damit ist die Begründung endgültig zu einer Zeichenkette geworden, auf die sich nicht mehr verzweigen lässt.
Der dritte Preis fällt im Betrieb an. Eine Meldung ohne Zusammenhang ist wertlos. Dass eine Datei nicht gefunden wurde, hilft niemandem, solange nicht dabeisteht, welche Datei, welcher Konfigurationsschlüssel darauf gezeigt hat und welche Anfrage gerade bearbeitet wurde. Diese Information liegt im Moment des Scheiterns vor und wird weggeworfen, weil niemand sie anhängt.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Eine fehlbare Funktion, Stück für Stück
Die Funktion liest eine Datei und wandelt ihren Inhalt in eine Konfiguration um. Zwei verschiedene Fehlertypen laufen auf, und beide passen in dieselbe Signatur, sobald die Umwandlung definiert ist.
Der Aufbau
fn lade(pfad: &Path) -> Result<Konfig, LadeFehler> {
let text = std::fs::read_to_string(pfad)?;
let konfig: Konfig = serde_json::from_str(&text)?;
Ok(konfig)
} - 01 Der Rückgabetyp
-> Result<Konfig, LadeFehler>Die Signatur sagt, dass die Funktion scheitern kann und mit welchem Fehlertyp. Wer den Rückgabewert nicht auswertet, bekommt eine Warnung, weil Result entsprechend markiert ist. Das ist der Unterschied zu einem Fehlercode, den man übersehen kann.
- 02 Der ausgepackte Erfolgswert
let textBei Erfolg bekommst du den Inhalt selbst, hier den Dateiinhalt als Text, und nicht das Result darum herum. Das Auspacken übernimmt der Operator am Ende der Zeile, deshalb steht hier kein zusätzlicher Zugriff.
- 03 Der erste fehlbare Aufruf
std::fs::read_to_string(pfad)?Der Aufruf liefert entweder den Text oder einen Ein- und Ausgabefehler. Das Fragezeichen gibt bei Erfolg den Wert weiter und verlässt bei einem Fehler die Funktion sofort, ohne dass eine Verzweigung nötig wäre.
- 04 Der zweite Aufruf mit anderem Fehlertyp
serde_json::from_str(&text)?Hier kommt ein anderer Fehlertyp zurück als in der Zeile darüber. Das Fragezeichen wandelt ihn über die passende Konvertierung in LadeFehler um, und genau deshalb passen beide Zeilen in dieselbe Signatur. Fehlt diese Konvertierung, lehnt der Compiler das Fragezeichen ab.
- 05 Der Erfolgsfall am Ende
Ok(konfig)Der Erfolgswert wird ausdrücklich verpackt, sonst passt der Typ nicht. Beim Lesen ist genau daran zu erkennen, an welcher Stelle eine Funktion endet, ohne gescheitert zu sein.
Wenn es nicht funktioniert
Das siehst du
Der Fragezeichen-Operator wird abgelehnt, weil sich der Fehlertyp aus dem Aufruf nicht in den Fehlertyp der Funktion umwandeln lässt.
Warum
Das Fragezeichen ruft im Hintergrund eine Konvertierung auf. Fehlt sie für diesen Fehlertyp, kann der Compiler die Umwandlung nicht durchführen und lehnt die Zeile ab.
Was hilft
Die Konvertierung für den fremden Fehlertyp schreiben oder sie sich von thiserror erzeugen lassen, indem das entsprechende Feld in der eigenen Aufzählung markiert wird. In einer Anwendung genügt oft der Sammeltyp von anyhow, der jeden gängigen Fehler entgegennimmt.
Das siehst du
Das Fragezeichen lässt sich in der Hauptfunktion nicht verwenden.
Warum
Der Operator funktioniert nur in Funktionen, die Result oder Option zurückgeben. Eine Hauptfunktion ohne Rückgabetyp erfüllt das nicht.
Was hilft
Die Hauptfunktion auf einen Result-Rückgabetyp umstellen und am Ende den leeren Erfolgswert zurückgeben. Der Rückgabewert wird ausgewertet: Bei einem Fehler gibt das Programm dessen Darstellung aus und beendet sich mit einem Fehlercode.
Das siehst du
Ein Aufruf liefert Option, die Funktion gibt aber Result zurück, und das Fragezeichen passt nicht.
Warum
Der Operator wandelt nicht zwischen Option und Result um. Beide beschreiben verschiedene Dinge: Option eine vorgesehene Abwesenheit, Result ein Scheitern mit Begründung.
Was hilft
Aus dem leeren Fall mit ok_or oder ok_or_else einen Fehler machen, dann greift das Fragezeichen. In die andere Richtung liefert ok aus einem Result eine Option und wirft die Begründung dabei weg, was du bewusst entscheiden solltest.
Das siehst du
Das Programm bricht im Betrieb mit einer Meldung ab, die nur besagt, dass unwrap auf einem leeren Wert aufgerufen wurde, und niemand weiß, welcher Aufruf gemeint war.
Warum
Unwrap bricht ohne Zusammenhang ab. Die Meldung nennt Datei und Zeile, aber nicht, welcher Datensatz, welcher Pfad oder welche Konfiguration den Fall ausgelöst hat.
Was hilft
Statt unwrap expect mit einem Satz verwenden, der die Annahme benennt, oder den Fall richtig behandeln. Bei Fehlern, die weitergereicht werden, mit anyhow auf jeder Ebene Kontext anhängen, dann steht am Ende die ganze Kette im Protokoll.
Das siehst du
Der Compiler warnt, dass ein Result nicht verwendet wird.
Warum
Result ist so markiert, dass ein ungenutzter Rückgabewert eine Warnung auslöst. Sie erscheint typischerweise bei Aufrufen, die man nur wegen ihrer Wirkung hinschreibt, etwa beim Schreiben, beim Leeren eines Puffers oder beim Löschen einer Datei.
Was hilft
Den Fehler mit dem Fragezeichen weiterreichen, ihn behandeln, oder ihn ausdrücklich verwerfen. Für das bewusste Verwerfen gibt es die Zuweisung an den Unterstrich, die beim Lesen sichtbar macht, dass hier eine Entscheidung getroffen wurde.
Fünf Entscheidungen führen zur richtigen Fehlerbehandlung
- 01 Ist das Fehlen vorgesehen oder ein Scheitern? Danach fällt Option oder Result.
- 02 Schreibst du eine Bibliothek? Dann braucht der Aufrufer eine Aufzählung zum Verzweigen.
- 03 Schreibst du eine Anwendung? Dann reicht ein Typ mit Kontext, etwa aus anyhow.
- 04 Kann der Aufrufer auf den Fall reagieren? Sonst braucht er keine eigene Variante.
- 05 Weiß nur diese Ebene den Pfad oder die Kennung? Dann hängt sie den Kontext hier an.
Was du danach ohne Nachdenken richtig aufsetzt
Die Entscheidungen sind wenige und lassen sich einmal pro Projekt treffen: welcher Typ für Abwesenheit, welcher für Scheitern, welche Bibliothek in Bibliotheken und welche in Anwendungen, wo Kontext angehängt wird und wo ein Abbruch vertretbar ist.
Option und Result auseinanderhalten
Option steht für ein Fehlen, das zum Modell gehört. Result steht für ein Scheitern mit Begründung. Wenn du an vielen Stellen aus einem fehlenden Wert einen Fehler machst, hätte die Funktion vermutlich von vornherein ein Result zurückgeben sollen.
Den Fragezeichen-Operator statt Verzweigung
Er gibt bei Erfolg den ausgepackten Wert und verlässt bei einem Fehler sofort die Funktion, wobei er den Fehler in den erwarteten Typ umwandelt. Das ist der ganze Mechanismus, und er ist der Grund, warum fünf fehlbare Aufrufe hintereinander lesbar bleiben.
Eigene Fehlertypen als Aufzählung
Eine Variante pro Ursache, auf die der Aufrufer unterschiedlich reagieren kann. Alles, was er ohnehin gleich behandelt, braucht keine eigene Variante. Diese Frage entscheidet über die Größe des Typs, nicht die Anzahl der Fehlerquellen im Code.
Thiserror in Bibliotheken, anyhow in Anwendungen
Eine Bibliothek muss ihren Aufrufern das Verzweigen erlauben, definiert also eine konkrete Aufzählung, und thiserror erzeugt die zugehörige Schreibarbeit. Eine Anwendung muss nur melden und abbrechen, dort genügt ein Typ mit Kontext. Umgekehrt ist es der klassische Fehler.
Abbrüche bewusst zulassen
Ein Abbruch ist richtig, wenn die Annahmen des Programms verletzt sind und Weitermachen schlimmer wäre, etwa bei einer ungültigen Konfiguration beim Start. Die ehrliche Form ist expect mit einem Satz, der die Annahme benennt, statt eines nackten unwrap.
Kontext anhängen, bevor der Fehler nach oben geht
Jede Ebene ergänzt, was nur sie weiß: den Pfad, die Kennung des Datensatzes, die Anfrage. Mit anyhow ist das ein Aufruf, bei einer eigenen Aufzählung ein Feld in der Variante. Ohne das steht am Ende im Protokoll eine Ursache ohne Fall.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Option und Result und wie man zwischen ihnen wechselt
Option kommt aus dem Nachschlagen in einer Zuordnung, aus dem ersten Element einer möglicherweise leeren Liste, aus einer Suche, die nichts findet. Der Compiler zwingt dich, den leeren Fall zu behandeln, bevor du an den Wert kommst, und dafür gibt es neben der Verzweigung eine Reihe von Kurzformen: einen Standardwert einsetzen, den Inhalt umformen, verketten oder filtern. Result kommt aus Ein- und Ausgabe, aus dem Umwandeln von Text in Zahlen, aus Netzwerkaufrufen.
Der Wechsel zwischen beiden ist eine bewusste Entscheidung und keine Formalität. Aus einem leeren Wert einen Fehler zu machen bedeutet, eine Begründung zu erfinden, und die sollte etwas taugen. In die andere Richtung wirft der Wechsel die Begründung weg, was genau dann richtig ist, wenn der Aufrufer sie ohnehin nicht braucht. Es gibt außerdem Funktionen, die eine Verschachtelung der beiden Typen umdrehen, was beim Verarbeiten von Listen mit fehlbaren Schritten regelmäßig gebraucht wird.
Die praktikable Faustregel: Sobald der Aufrufer wissen wollen könnte, warum kein Wert da ist, gehört ein Result hin. Wenn die Antwort immer nur lautet, dass es den Eintrag eben nicht gibt, ist Option richtig und ein Fehlertyp wäre Aufwand ohne Ertrag.
Was der Fragezeichen-Operator tatsächlich tut
Bei einem Erfolgswert liefert er den Inhalt. Bei einem Fehler verlässt er die Funktion und gibt den Fehler zurück, wobei er ihn vorher in den Fehlertyp der Funktion umwandelt. Bei einem leeren Wert in einer Funktion, die Option zurückgibt, verlässt er sie mit dem leeren Wert. Mehr passiert nicht, und das ist der Grund, warum eine Kette aus fünf fehlbaren Aufrufen ohne eine einzige Verzweigung lesbar bleibt.
Die Umwandlung ist der Teil, den man am Anfang übersieht. Fünf verschiedene Bibliotheken liefern fünf verschiedene Fehlertypen, und alle passen in dieselbe Signatur, sobald es für jeden eine Konvertierung in deinen eigenen Typ gibt. Diese Konvertierungen von Hand zu schreiben ist stumpfe Arbeit, und genau die nimmt einem thiserror ab, indem es sie aus einer Markierung an der Variante erzeugt.
Was der Operator nicht tut, ist Kontext ergänzen. Der Ein- und Ausgabefehler, der bei dir ankommt, sagt, dass eine Datei nicht existiert, nicht welche. Wenn diese Information nicht an genau der Stelle angehängt wird, an der sie bekannt ist, ist sie weg. Deshalb gehört zu jeder Ebene, die einen Fehler durchreicht, die Frage, ob sie etwas weiß, das die darüberliegende nicht mehr weiß.
Eigene Fehlertypen: was in eine Variante gehört
Der Maßstab ist nicht, wie viele Fehlerquellen es gibt, sondern auf wie viele Fälle ein Aufrufer unterschiedlich reagieren kann. Wenn er bei einer fehlenden Datei und bei fehlenden Rechten dasselbe tut, nämlich abbrechen und melden, reicht eine Variante. Wenn er bei einem Zeitüberschreitungsfehler einen erneuten Versuch startet und bei einem Formatfehler nicht, sind das zwei.
Ein brauchbarer Fehlertyp liefert drei Dinge: eine Meldung, die ein Mensch lesen kann, den zugrunde liegenden Fehler, damit sich die Kette rekonstruieren lässt, und genug Daten in den Feldern, um darauf zu verzweigen. Der dritte Punkt ist derjenige, der am häufigsten fehlt, weil die Information in der Textmeldung landet statt in einem Feld.
Der Anti-Fall ist eine öffentliche Schnittstelle, die einen Sammeltyp oder gleich eine Zeichenkette als Fehler zurückgibt. Beides übersetzt anstandslos und nimmt dem Aufrufer jede Möglichkeit, angemessen zu reagieren. In einer Bibliothek ist die Fehleraufzählung Teil der Schnittstelle, ihre Änderung ist ein Bruch, und dieser Umstand zwingt beim Entwurf zu der nützlichen Frage, welche Fälle du eigentlich zusagen willst.
Thiserror in Bibliotheken, anyhow in Anwendungen
In einer Bibliothek müssen Aufrufer verzweigen können. Also definierst du eine konkrete Aufzählung, und thiserror erzeugt daraus die Darstellung für Menschen, die Konvertierungen aus fremden Fehlertypen und die Verkettung zur Ursache. Du schreibst die Aufzählung und die Meldungstexte, den Rest erzeugt das Makro.
In einer Anwendung verzweigt niemand auf deine Fehler, sie werden gelesen. Dafür ist ein einziger Sammeltyp die richtige Wahl, und der Nutzen liegt im Anhängen von Kontext: Auf jeder Ebene kommt ein Satz dazu, der beschreibt, was gerade versucht wurde. Am Ende steht im Protokoll eine Kette von der Anfrage bis zum Systemfehler, und die beantwortet die meisten Fragen ohne Rückfrage beim Entwicklungsteam.
Beides in einem Projekt ist der Normalfall und kein Widerspruch: Die internen Bibliotheks-Pakete definieren ihre Aufzählungen, das ausführbare Paket sammelt sie ein, hängt Kontext an und macht daraus einen Bericht. Falsch herum wird es teuer. Eine Bibliothek, die den Sammeltyp nach außen gibt, zwingt jeden Aufrufer dazu, auf Textmeldungen zu prüfen, und diese Prüfung bricht beim nächsten Wortlaut.
Wo ein Abbruch vertretbar ist und wie Fehler nach außen aussehen
Vertretbar ist ein Abbruch in Tests, in kurzlebigen Hilfsprogrammen, beim Start mit einer ungültigen Konfiguration und bei einer verletzten Zusicherung im eigenen Code, bei der Weiterlaufen bedeutet, mit falschen Daten weiterzuarbeiten. In diesen Fällen ist lautes Scheitern die richtige Antwort, und expect mit einem erklärenden Satz macht daraus eine Meldung, die jemand versteht.
Nicht vertretbar ist ein Abbruch, der sich von außen auslösen lässt. Eine Anfrage mit einer fehlerhaften Nutzlast darf keinen Prozess umbringen. In einem Serverdienst ist das keine akademische Frage: Ein Abbruch innerhalb einer Aufgabe beendet diese Aufgabe, und je nach Aufbau reißt er mehr mit, als einem lieb ist. Alles, was aus Eingaben kommt, gehört deshalb über Result behandelt.
An der Außengrenze wird übersetzt. Eine HTTP-Schnittstelle gibt nicht deine Aufzählung zurück, sondern einen Statuscode und einen Rumpf. Diese Zuordnung triffst du bewusst: welche Varianten sind ein Eingabefehler, welche ein fehlender Datensatz, welche ein interner Fehler. Was dabei keinesfalls passieren darf, ist das Durchreichen der internen Meldung an den Aufrufer, denn dort stehen Pfade, Abfragen und gelegentlich Zugangsdaten. Und protokolliert wird einmal, auf der Ebene, die entscheiden kann, dafür mit der vollständigen Kette. Auf jeder Ebene zu protokollieren erzeugt fünf Einträge für einen Vorfall und keine zusätzliche Information.
Dazu passende Kurse
Wenn ihr diese Struktur einmal gemeinsam aufsetzen wollt, statt sie pro Modul neu zu erfinden, gibt es dafür Rust-Kurse mit Übungen an echtem Code .
Weil sich viele dieser Fälle erst unter Last zeigen, gehört zur Fehlerbehandlung ein Testaufbau, der sie überhaupt auslöst, und dazu gibt es Kurse zu Softwaretest und Qualitätssicherung .
Wie sicher bist du beim Thema wirklich?
Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.
Wo du genau das übst
Sehr angenehmes, gut vorbereitetes Seminar, kann ich nur empfehlen.
Häufige Fragen
Ist unwrap immer falsch?
Was kostet Result an Laufzeit?
Brauche ich für jedes Modul einen eigenen Fehlertyp?
Wie bekomme ich einen Aufrufverlauf zu einem Fehler?
Deine Ansprechpartner
Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.
Yves Hoppe
Weiterbildung & Beratung
Hilft dir, aus dem Rust-Programm den passenden Kurs für deinen Stand zu finden.
Norbert Jansen
Beratung & Inhouse
Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.
Fehlerbehandlung, die im Betrieb etwas aussagt
In den Rust-Kursen bei cmt baust du eigene Fehlertypen auf und siehst, an welcher Stelle Kontext angehängt gehört, damit eine Meldung im Protokoll noch etwas wert ist.