Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Kapitel 9: Systematische Fehlerbehandlung – Der Umgang mit dem Unerwarteten

Willkommen in einem der wichtigsten Kapitel auf deiner Reise mit Rust! Stell dir vor, du baust ein Baumhaus. Du hast alles perfekt geplant, die Bretter sind zugeschnitten und die Schrauben liegen bereit. Aber was passiert, wenn es plötzlich mitten im Bauen anfängt zu stürmen? Oder wenn dir eine Schraube ins hohe Gras fällt und unauffindbar ist?

In der echten Welt müssen wir flexibel sein und auf unvorhergesehene Ereignisse reagieren. Genau so ist es auch beim Programmieren. Programme laufen nicht immer in einer perfekten Laborumgebung. Manchmal möchte ein Benutzer eine Datei öffnen, die gar nicht existiert. Ein anderes Mal verliert der Computer mitten in einer Berechnung die Internetverbindung.

Rust ist weltberühmt dafür, wie sicher es mit solchen Situationen umgeht. Es zwingt uns Entwickler dazu, uns vorher Gedanken über mögliche Fehler zu machen. In diesem Kapitel lernst du, wie Rust Fehler einteile und wie du sie meisterst, als wärst du ein Detektiv, der auf alles vorbereitet ist.


Die zwei Arten von Fehlern

In Rust gibt es zwei grundlegend verschiedene Arten von Fehlern:

  1. Unheilbare Fehler (Unrecoverable Errors): Das sind Fehler, bei denen das Programm absolut nicht mehr weiterarbeiten kann. Wenn der Computer keinen Arbeitsspeicher mehr hat oder eine absolut lebenswichtige Komponente fehlt, gibt Rust auf. Das nennen wir eine Panic (Panik).
  2. Heilbare Fehler (Recoverable Errors): Das sind alltägliche Missgeschicke. Eine Datei wurde nicht gefunden, eine Zahl wurde durch Null geteilt oder ein Benutzer hat sein Passwort falsch eingegeben. Hier wollen wir nicht, dass das Programm sofort abstürzt. Stattdessen möchten wir dem Benutzer eine freundliche Fehlermeldung zeigen oder es einfach noch einmal versuchen.

Schauen wir uns diese Konzepte mit einfachen Analogien aus dem echten Leben an!


1. Panic als Notbremse in der Achterbahn (Unheilbare Fehler)

Stell dir vor, du sitzt in einer Achterbahn und saust mit Highspeed durch Loopings. Die Achterbahn hat viele eingebaute Sicherheitsprüfungen. Wenn die Sensoren der Bahn merken, dass ein wichtiges Laufrad abgebrochen oder die Schiene beschädigt ist, gibt es nur eine vernünftige Reaktion: Die Notbremse ziehen und die Achterbahn sofort stoppen!

Es wäre lebensgefährlich zu sagen: “Ach, fahren wir einfach mal weiter und gucken, was passiert.” Der sofortige Stopp verhindert Schlimmeres.

In Rust entspricht diese Notbremse dem Makro panic!. Wenn dein Programm in eine Situation gerät, aus der es unmöglich sicher entkommen kann, bricht Rust das Programm augenblicklich ab und gibt eine Meldung aus.

Ein echtes Code-Beispiel für eine Notbremse

Schauen wir uns an, wie wir eine solche Notbremse absichtlich im Code auslösen können.

fn main() {
    println!("Die Achterbahn startet die Fahrt...");

    // Wir simulieren eine Überprüfung der Räder
    let raeder_in_ordnung = false;

    if !raeder_in_ordnung {
        // Wenn die Räder nicht in Ordnung sind, ziehen wir die Notbremse!
        panic!("NOTBREMSE! Ein Rad ist locker! Die Fahrt wird sofort abgebrochen.");
    }

    // Dieser Code wird niemals erreicht, wenn die Räder kaputt sind
    println!("Wir fahren durch den Looping! Juhu!");
}

Zeilenweise Erklärung des Codes

  • fn main() { ... }: Das ist der Einstiegspunkt unseres Programms. Hier fängt der Computer an zu lesen.
  • println!("Die Achterbahn startet die Fahrt...");: Wir geben einen Text auf dem Bildschirm aus, damit wir sehen, dass das Programm läuft.
  • let raeder_in_ordnung = false;: Wir erstellen eine unveränderliche Variable namens raeder_in_ordnung und weisen ihr den Wahrheitswert false (falsch) zu. Das bedeutet, dass etwas mit den Rädern nicht stimmt.
  • if !raeder_in_ordnung { ... }: Das Ausrufezeichen ! steht für das logische “NICHT”. Wir prüfen also: “Wenn die Räder NICHT in Ordnung sind, dann…”
  • panic!("NOTBREMSE!..."): Hier rufen wir das Panic-Makro auf. Sobald das Programm an diese Zeile gelangt, stoppt es sofort. Es gibt den Text in den Anführungszeichen aus und beendet sich.
  • println!("Wir fahren durch den Looping! Juhu!");: Weil das Programm in der Zeile darüber abgebrochen wurde, wird diese Zeile niemals ausgeführt. Rust schützt uns davor, mit einer kaputten Achterbahn weiterzufahren!

2. Option<T> als Kaugummiautomat (Es könnte da sein oder auch nicht)

Manchmal ist ein Fehler gar kein “schlimmer” Fehler, sondern einfach das Fehlen von etwas.

Stell dir einen klassischen roten Kaugummiautomat vor. Du wirfst eine Münze ein und drehst am Rad. Nun gibt es genau zwei Möglichkeiten:

  1. Es kommt ein Kaugummi heraus: Du hältst einen leckeren Kaugummi in der Hand. In Rust nennen wir das Some(Kaugummi) (was übersetzt so viel heißt wie “Hier ist etwas!”).
  2. Der Automat ist leer: Du hörst nur ein hohles Klackern und es kommt nichts heraus. In Rust nennen wir das None (übersetzt “Nichts”).

Rust hat für genau diese Situationen einen eingebauten Datentyp namens Option\<T\>. Das \<T\> ist ein Platzhalter für den Typ der Sache, die wir erwarten (zum Beispiel ein Kaugummi oder ein Text String).

In vielen anderen Programmiersprachen gibt es für solche Fälle das Wort null oder nil. Das führt oft zu riesigen Problemen, weil Programmierer vergessen zu prüfen, ob überhaupt etwas da ist, und das Programm dann abstürzt (die berüchtigte “NullPointerException”). In Rust ist das unmöglich! Rust zwingt dich dazu, den Karton des Kaugummiautomaten erst zu öffnen und nachzusehen, ob ein Kaugummi drin ist.

Ein echtes Code-Beispiel für den Kaugummiautomaten

Lass uns diesen Kaugummiautomaten in Rust nachbauen!

// Wir definieren einen Kaugummiautomaten
struct Kaugummiautomat {
    anzahl_kaugummis: u32,
}

impl Kaugummiautomat {
    // Diese Methode gibt uns vielleicht einen Kaugummi
    fn drehen(&mut self) -> Option<String> {
        if self.anzahl_kaugummis > 0 {
            // Wir ziehen einen Kaugummi ab
            self.anzahl_kaugummis -= 1;
            // Wir geben einen Kaugummi zurück, verpackt in "Some"
            Some(String::from("Erdbeer-Kaugummi"))
        } else {
            // Der Automat ist leer, wir geben "None" zurück
            None
        }
    }
}

fn main() {
    // Wir bauen einen Automaten mit nur 1 Kaugummi
    let mut mein_automat = Kaugummiautomat { anzahl_kaugummis: 1 };

    // Erster Dreh: Es sollte ein Kaugummi kommen!
    match mein_automat.drehen() {
        Some(kaugummi) => println!("Lecker! Ich habe einen {} bekommen!", kaugummi),
        None => println!("Schade, der Automat ist leider leer."),
    }

    // Zweiter Dreh: Jetzt ist der Automat leer!
    match mein_automat.drehen() {
        Some(kaugummi) => println!("Lecker! Ich habe einen {} bekommen!", kaugummi),
        None => println!("Schade, der Automat ist leider leer."),
    }
}

Zeilenweise Erklärung des Codes

  • struct Kaugummiautomat { anzahl_kaugummis: u32 }: Wir erstellen einen Bauplan für unseren Automaten. Er merkt sich die Anzahl der Kaugummis als positive Ganzzahl (u32).
  • impl Kaugummiautomat { ... }: Hier schreiben wir die Funktionen (Methoden), die zu unserem Automaten gehören.
  • fn drehen(&mut self) -> Option<String>: Die Methode drehen verändert den Automaten (daher &mut self, weil sich die Anzahl der Kaugummis verringert). Sie gibt ein Option\<String\> zurück. Das bedeutet: Entweder bekommen wir einen Text (Some(String)) oder eben nichts (None).
  • if self.anzahl_kaugummis > 0 { ... } else { ... }: Wir prüfen, ob noch Kaugummis da sind.
    • Wenn ja, ziehen wir einen ab (self.anzahl_kaugummis -= 1) und geben ihn eingepackt in Some(...) zurück.
    • Wenn nein, geben wir None zurück.
  • let mut mein_automat = ...: In der main-Funktion erstellen wir unseren Automaten. Er muss veränderbar (mut) sein, weil wir an ihm drehen und sich die Anzahl der Kaugummis ändert.
  • match mein_automat.drehen() { ... }: Das match-Wort ist wie eine Weiche bei der Eisenbahn. Es prüft, welchen “Weg” das Ergebnis nimmt:
    • Some(kaugummi) => ...: Wenn das Ergebnis Some ist, packt Rust den Kaugummi-Text aus und gibt ihm den Namen kaugummi. Diesen können wir dann im Text ausdrucken.
    • None => ...: Wenn der Automat leer war, wird dieser block ausgeführt.

3. Result<T, E> als Paketlieferung (Erfolg oder Schadenbericht)

Was ist, wenn wir genauer wissen wollen, warum etwas schiefgelaufen ist? Wenn der Kaugummiautomat leer ist, ist das einfach. Aber wenn wir ein Paket im Internet bestellen, wollen wir wissen, ob es ankommt oder ob unterwegs etwas Schlimmes passiert ist.

Stell dir vor, du bestellst einen funkelnden neuen Laptop im Internet. Der Postbote klingelt an deiner Tür und übergibt dir ein Paket. Es gibt zwei mögliche Zustände dieses Pakets:

  1. Alles ist super (Ok): Du machst das Paket auf und darin liegt der bestellte Laptop. Du kannst ihn sofort benutzen. In Rust schreiben wir das als Ok(Laptop).
  2. Es gab einen Fehler (Err): Der Karton ist völlig zerquetscht, nass und zerrissen. Der Laptop ist kaputt. Statt des Laptops liegt ein Zettel der Post dabei: “Entschuldigung, das Paket ist beim Transport in einen Fluss gefallen.” In Rust schreiben wir das als Err(KartonKaputt).

Das Result\<T, E\> ist genau wie dieses Paket. Es hat zwei Seiten:

  • T steht für den Erfolgswert (den Laptop), der in ein Ok eingepackt ist.
  • E steht für den Fehlerwert (den Schadensbericht), der in ein Err eingepackt ist.

Ein echtes Code-Beispiel für die Paketlieferung

Schauen wir uns an, wie wir ein Paket in Rust bestellen und öffnen:

// Die verschiedenen Dinge, die bei der Lieferung schiefgehen können
#[derive(Debug)]
enum LieferFehler {
    KartonKaputt,
    PostboteVerlaufen,
    AdresseNichtGefunden,
}

// Unsere Funktion simuliert den Versand
fn paket_versenden(adresse: &str) -> Result<String, LieferFehler> {
    if adresse == "Unbekannte Str. 99" {
        // Die Adresse existiert nicht!
        return Err(LieferFehler::AdresseNichtGefunden);
    } else if adresse == "Waldweg 5" {
        // Der Postbote findet den Weg im tiefen Wald nicht
        return Err(LieferFehler::PostboteVerlaufen);
    }

    // Wenn alles klappt, schicken wir den Laptop!
    Ok(String::from("Glänzender neuer Laptop"))
}

fn main() {
    let empfaenger_adresse = "Waldweg 5";

    // Wir versuchen, das Paket zu empfangen
    match paket_versenden(empfaenger_adresse) {
        Ok(inhalt) => {
            println!("Juhu! Mein Paket ist da. Inhalt: {}", inhalt);
        }
        Err(fehler) => {
            // Wenn ein Fehler auftritt, schauen wir uns den Schadensbericht an
            match fehler {
                LieferFehler::KartonKaputt => {
                    println!("Oh nein! Der Karton ist kaputt gegangen.");
                }
                LieferFehler::PostboteVerlaufen => {
                    println!("Der Postbote hat sich im Wald verlaufen!");
                }
                LieferFehler::AdresseNichtGefunden => {
                    println!("Diese Adresse gibt es gar nicht.");
                }
            }
        }
    }
}

Zeilenweise Erklärung des Codes

  • enum LieferFehler { ... }: Wir erstellen ein Enum (eine Aufzählung) für alle Fehler, die passieren können. Das #[derive(Debug)] darüber erlaubt es Rust, diese Fehler später einfach auf den Bildschirm zu drucken, falls wir das möchten.
  • fn paket_versenden(adresse: &str) -> Result<String, LieferFehler>: Diese Funktion nimmt eine Adresse als Text an und gibt ein Result zurück.
    • Im Erfolgsfall (Ok) bekommen wir einen Text String (unseren Laptop).
    • Im Fehlerfall (Err) bekommen wir einen LieferFehler aus unserem Enum.
  • return Err(...): Wenn die Adresse falsch ist, brechen wir die Funktion sofort mit return ab und geben den entsprechenden Fehler eingepackt in Err zurück.
  • Ok(String::from("...")): Wenn keine der Fehlerbedingungen zutrifft, packen wir den Laptop-Text in ein Ok und geben ihn zurück.
  • match paket_versenden(...): In der main-Funktion nutzen wir wieder das match-Wort, um das Paket auszupacken.
    • Weg 1: Ok(inhalt) -> Wir freuen uns über den Inhalt.
    • Weg 2: Err(fehler) -> Wir müssen den Fehler genauer untersuchen. Mit einem zweiten, inneren match prüfen wir, welcher der drei Fehler aus dem Enum aufgetreten ist, und reagieren passend darauf.

4. Der ?-Operator (Der “Weitergabe-Seufzer” oder “Postbote-weiterleiten-Trick”)

Stell dir vor, du bist ein Mitarbeiter in einer großen Firma. Dein Chef gibt dir eine Aufgabe: “Bestell mir einen neuen Arbeits-Laptop. Sobald er da ist, installiere ein Schreibprogramm darauf und bring mir den fertigen Laptop.”

Du bestellst also das Paket. Der Postbote bringt es dir. Jetzt könntest du jedes Mal das Paket selbst mühsam aufmachen, prüfen ob der Laptop ganz ist, ihn rausholen, das Programm installieren und so weiter.

Aber es gibt einen viel einfacheren Trick: den Weitergabe-Seufzer. Wenn das Paket bei dir ankommt und der Karton ist völlig zerquetscht (Err), seufzt du kurz, machst gar nicht erst weiter und reichst das kaputte Paket ungeöffnet direkt an deinen Chef weiter: “Chef, hier ist das Paket. Es ist kaputt. Kümmer du dich darum!”

Nur wenn das Paket unbeschädigt ist (Ok), machst du es auf, nimmst den Laptop heraus und machst deine Arbeit weiter.

In Rust ist das Fragezeichen ? genau dieser Trick. Wenn du ein Result oder Option hast und das Fragezeichen dahintersetzt, sagt Rust:

  • Wenn es ein Erfolg (Ok oder Some) ist: Pack es aus und gib mir direkt den Inhalt!
  • Wenn es ein Fehler (Err oder None) ist: Stoppe diese Funktion sofort und schicke den Fehler direkt an denjenigen zurück, der diese Funktion aufgerufen hat!

Ein echtes Code-Beispiel für den ?-Operator

Schauen wir uns an, wie viel kürzer und schöner unser Code mit dem ?-Operator wird. Wir simulieren eine Kette: Paket bestellen -> Laptop auspacken -> Programm installieren.

#[derive(Debug)]
enum BueroFehler {
    PaketVerloren,
    InstallationFehlgeschlagen,
}

// Schritt 1: Das Paket bestellen
fn laptop_bestellen() -> Result<String, BueroFehler> {
    // Wir simulieren einen Erfolg
    Ok(String::from("Neuer Laptop"))
}

// Schritt 2: Das Programm auf dem Laptop installieren
fn programm_installieren(laptop: String) -> Result<String, BueroFehler> {
    // Wir fügen das Programm hinzu
    let fertiger_laptop = format!("{} mit installiertem Schreibprogramm", laptop);
    Ok(fertiger_laptop)
}

// Die Hauptaufgabe, die beide Schritte kombiniert
fn arbeitsplatz_einrichten() -> Result<String, BueroFehler> {
    // Hier nutzen wir das Fragezeichen!
    // Wenn 'laptop_bestellen' einen Fehler liefert, bricht 'arbeitsplatz_einrichten'
    // sofort ab und gibt den Fehler zurück. Wenn nicht, wird der Laptop direkt in
    // die Variable 'laptop' ausgepackt.
    let laptop = laptop_bestellen()?;

    // Auch hier nutzen wir das Fragezeichen für den nächsten Schritt
    let fertiges_geraet = programm_installieren(laptop)?;

    // Wenn alles geklappt hat, geben wir das fertige Gerät zurück
    Ok(fertiges_geraet)
}

fn main() {
    match arbeitsplatz_einrichten() {
        Ok(geraet) => println!("Erfolg! Der Arbeitsplatz hat einen: {}", geraet),
        Err(fehler) => println!("Arbeitsplatz konnte nicht eingerichtet werden: {:?}", fehler),
    }
}

Zeilenweise Erklärung des Codes

  • let laptop = laptop_bestellen()?;: Das ist die Zauberzeile! laptop_bestellen() gibt ein Result\<String, BueroFehler\> zurück.
    • Ohne das ? müssten wir hier ein langes match schreiben, um zu prüfen, ob es ein Ok oder Err ist.
    • Mit dem ? packt Rust im Erfolgsfall den String aus und speichert ihn direkt in der Variable laptop. Wenn ein Fehler auftritt, beendet Rust die Funktion arbeitsplatz_einrichten sofort an dieser Stelle und reicht den Fehler nach oben an die main-Funktion weiter.
  • let fertiges_geraet = programm_installieren(laptop)?;: Genau dasselbe passiert hier. Wir reichen den ausgepackten laptop weiter. Wenn die Installation fehlschlägt, brechen wir ab. Wenn nicht, haben wir das fertige Gerät.
  • Ok(fertiges_geraet): Weil die Funktion arbeitsplatz_einrichten versprochen hat, ein Result zurückzugeben, müssen wir das fertige Gerät am Ende wieder in ein Ok einpacken.

Zusammenfassung: Dein Spickzettel für Fehler

KonzeptAnalogieWann benutzt man es?
panic!Notbremse in der AchterbahnBei unheilbaren Fehlern, wenn das Programm absolut nicht mehr weiterlaufen darf.
Option\<T\>Kaugummiautomat (Some / None)Wenn etwas vorhanden sein kann oder eben nicht (z.B. ein optionaler Name).
Result\<T, E\>Paketlieferung (Ok / Err)Wenn eine Aktion fehlschlagen kann und wir wissen wollen, warum (z.B. Datei nicht gefunden).
? (Fragezeichen)Postbote-weiterleiten-TrickUm Fehler blitzschnell an die übergeordnete Funktion weiterzureichen, ohne alles selbst auszupacken.

Mit diesen Werkzeugen bist du nun bestens gerüstet. Rust passt auf dich auf und sorgt dafür, dass dein Code stabil bleibt – selbst wenn draußen ein Sturm tobt oder ein Kaugummiautomat mal leer ist!