Result und Option: Fehlerbehandlung im Rust-Stil
1. Überblick
Rust hat keine Exceptions wie Java oder C#. Stattdessen nutzt Rust zwei Datentypen Option<T> und Result<T, E>, um “möglicherweise kein Wert” und “möglicherweise gescheitert” auszudrücken. Dieser Artikel hilft Ihnen, diese beiden Typen korrekt zu nutzen, Fehler elegant mit dem ?-Operator weiterzuleiten und benutzerdefinierte Fehler mit thiserror zu definieren. Nach dem Lesen können Sie Rust-Code mit klarer Fehlerbehandlung schreiben – ohne try/catch.
2. Warum keine Exceptions?
In Java kann eine Methode jederzeit RuntimeException werfen, ohne dass die Signatur dies ankündigt. Sie müssen Javadoc oder Code lesen, um herauszufinden, was sie werfen könnte.
Rust zwingt Sie, die Möglichkeit des Fehlschlags direkt im Rückgabetyp zu deklarieren:
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
return Err("Division durch Null".to_string());
}
Ok(a / b)
}
Ein Blick auf die Signatur Result<i32, String> verrät sofort, dass diese Funktion möglicherweise fehlschlagen kann. Der Compiler zwingt Sie, beide Branches Ok und Err zu behandeln – Sie können Fehler nicht stillschweigend übergehen wie ein vergessenes catch in Java.
Hinweis: Rust hat immer noch
panic!für nicht wiederherstellbare Fehler (Programmfehler, verletzte Invarianten).Resultist für vorhersehbare, behandelbare Fehler gedacht.
3. Option: Möglicherweise fehlende Werte
Option<T> ersetzt null. Es hat zwei Varianten: Some(T) und None.
fn find_customer(id: u32) -> Option<String> {
let customers = vec![(1, "Alice"), (2, "Bob")];
customers.iter()
.find(|(cid, _)| *cid == id)
.map(|(_, name)| name.to_string())
}
fn main() {
match find_customer(2) {
Some(name) => println!("Gefunden: {name}"),
None => println!("Kein Kunde vorhanden"),
}
}
find gibt Option<&(u32, &str)> zurück, map transformiert den Wert innen in Some, wenn vorhanden, und ignoriert ihn, wenn es None ist. Ergebnis:
Gefunden: Bob
3.1. Häufig verwendete Methoden
let maybe_age: Option<u32> = Some(25);
let age = maybe_age.unwrap_or(18); // 25, oder 18 wenn None
let doubled = maybe_age.map(|a| a * 2); // Some(50)
let result: Result<u32, &str> = maybe_age.ok_or("Alter nicht angegeben"); // Ok(25)
unwrap_or(default): Den Wert abrufen oder den Standard zurückgeben, panics niemals.map(f): Den Wert innen transformieren, wenn vorhanden,Noneunverändert lassen, wenn nicht.ok_or(err): KonvertiertOption<T>zuResult<T, E>– nützlich, wenn Sie den?-Operator später verwenden möchten.
Hinweis: Vermeiden Sie direkte Aufrufe von
.unwrap()aufOption/Resultim Production-Code. Es panics sofort beiNone/Err, äquivalent zum Werfen einer unkontrolliertenNullPointerException. Verwenden Sie es nur in Tests oder wenn Sie zu 100% sicher sind, dass der Wert vorhanden ist.
4. Result<T, E>: Erfolg oder Fehlschlag
Result<T, E> hat zwei Varianten: Ok(T) für Erfolg, Err(E) für Fehlschlag.
use std::num::ParseIntError;
fn parse_quantity(input: &str) -> Result<u32, ParseIntError> {
input.trim().parse::<u32>()
}
fn main() {
match parse_quantity(" 42 ") {
Ok(qty) => println!("Menge: {qty}"),
Err(e) => println!("Parse-Fehler: {e}"),
}
}
parse::<u32>() gibt Result<u32, ParseIntError> zurück. match zwingt Sie, beide Branches zu behandeln – der Compiler weigert sich, zu kompilieren, wenn ein Branch fehlt.
Menge: 42
4.1. Der ?-Operator zur Fehlerweiterleitung
Das Schreiben von match überall ist ausschweifend. Der ?-Operator hilft, Fehler sofort weiterzuleiten, wenn er auf Err trifft – ähnlich wie automatisches throw:
use std::num::ParseIntError;
struct Order {
quantity: u32,
price: u32,
}
fn parse_order(qty_str: &str, price_str: &str) -> Result<Order, ParseIntError> {
let quantity = qty_str.trim().parse::<u32>()?;
let price = price_str.trim().parse::<u32>()?;
Ok(Order { quantity, price })
}
fn main() {
match parse_order("3", "15000") {
Ok(order) => println!("Gesamtbetrag: {}", order.quantity * order.price),
Err(e) => println!("Ungültige Daten: {e}"),
}
}
Die Zeile qty_str.trim().parse::<u32>()? bedeutet: Wenn parse Err zurückgibt, gibt die Funktion parse_order sofort Err an den Aufrufer zurück, ohne manuell match schreiben zu müssen. Wenn Ok, wird der Wert innen “ausgepackt” und quantity zugewiesen.
Gesamtbetrag: 45000
Hinweis: Der
?-Operator kann nur verwendet werden, wenn die Fehlertypen der aktuellen und der aufgerufenen Funktion kompatibel sind (oder eineFrom-Implementierung zum Konvertieren vorhanden ist). Andernfalls meldet der Compiler einen Typ-Fehler während des Build.
5. Option und Result kombinieren
Diese beiden Typen werden oft vermischt. Zum Beispiel, um ein Produkt im Lager zu finden und dann zu überprüfen, ob der Bestand ausreicht:
struct Product {
name: String,
stock: u32,
}
fn find_product(name: &str, products: &[Product]) -> Option<&Product> {
products.iter().find(|p| p.name == name)
}
fn check_stock(name: &str, requested: u32, products: &[Product]) -> Result<(), String> {
let product = find_product(name, products)
.ok_or_else(|| format!("Produkt nicht gefunden: {name}"))?;
if product.stock < requested {
return Err(format!("Nur noch {} Produkte verfügbar", product.stock));
}
Ok(())
}
ok_or_else konvertiert Option<&Product> zu Result<&Product, String> mit Fehlermeldung, dann leitet ? weiter, wenn es None ist. Diese Schreibweise vermeidet tiefes, verschachteltes match.
6. Benutzerdefinierte Fehler mit thiserror definieren
Die Verwendung von String als Fehlertyp ist bequem für kleine Beispiele, aber reale Projekte benötigen strukturierte Error-Enums. Das Crate thiserror macht dies elegant:
<!-- Cargo.toml-Äquivalent, verwenden Sie dependencies section -->
# Cargo.toml
[dependencies]
thiserror = "1.0"
use thiserror::Error;
#[derive(Error, Debug)]
enum OrderError {
#[error("Produkt existiert nicht: {0}")]
ProductNotFound(String),
#[error("nicht genug Bestand, {available} Produkt(e) verfügbar")]
InsufficientStock { available: u32 },
}
fn validate_order(stock: Option<u32>, requested: u32) -> Result<(), OrderError> {
let available = stock.ok_or_else(|| OrderError::ProductNotFound("Laptop".into()))?;
if available < requested {
return Err(OrderError::InsufficientStock { available });
}
Ok(())
}
#[derive(Error, Debug)] generiert automatisch eine Implementierung des std::error::Error-Traits. #[error("...")] definiert die Meldung, die für jede Variante angezeigt wird, und unterstützt Platzhalter wie {0} (positionell) oder {available} (Feldname). Ergebnis beim Drucken des Fehlers:
Error: InsufficientStock { available: 2 }
Wenn Sie {} anstelle von {:?} beim Drucken verwenden, erhalten Sie eine benutzerfreundliche Meldung: nicht genug Bestand, 2 Produkt(e) verfügbar.
7. anyhow für Anwendungen, thiserror für Bibliotheken
Beim Schreiben einer Binary/Anwendung (nicht einer Bibliothek für andere) müssen Sie normalerweise nicht zwischen verschiedenen Fehlertypen unterscheiden – nur weitergeben und ausgeben. Das Crate anyhow eignet sich für diesen Fall:
[dependencies]
anyhow = "1.0"
use anyhow::{Context, Result};
fn load_config(path: &str) -> Result<String> {
std::fs::read_to_string(path)
.with_context(|| format!("Config-Datei konnte nicht gelesen werden: {path}"))
}
fn main() -> Result<()> {
let config = load_config("app.toml")?;
println!("Config: {config}");
Ok(())
}
anyhow::Result<T> ist ein Alias für Result<T, anyhow::Error> und kann jeden Fehler enthalten, der das Trait Error implementiert. with_context fügt zusätzliche Kontextinformationen zum ursprünglichen Fehler hinzu und erleichtert das Debugging, ohne eine eigene Enum definieren zu müssen.
- Bibliothek (Crate, die andere
usen werden): Verwenden Siethiserror, geben Sie ein spezifisches Error-Enum zurück, damit der Aufrufer jeden Fall mitmatchbehandelt. - Anwendung (endliche Binary): Verwenden Sie
anyhow, nur weitergeben und loggen, niemand muss detailliert mitmatcharbeiten.
8. Testen des Fehlerverhaltens
Rust hat ein integriertes Test-Framework, es bedarf keiner zusätzlichen Bibliotheken für grundlegende Tests.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn returns_ok_when_stock_sufficient() {
let result = validate_order(Some(10), 5);
assert!(result.is_ok());
}
#[test]
fn returns_err_when_insufficient_stock() {
let result = validate_order(Some(2), 5);
assert!(matches!(
result,
Err(OrderError::InsufficientStock { available: 2 })
));
}
#[test]
fn returns_err_when_product_not_found() {
let result = validate_order(None, 5);
assert!(matches!(result, Err(OrderError::ProductNotFound(_))));
}
}
#[test] markiert die Testfunktion, wird mit cargo test ausgeführt. matches! ist ein Macro, das überprüft, ob ein Wert einem vorgegebenen Muster entspricht – praktischer als assert_eq! beim Vergleichen von Enums mit Feldern, da Sie PartialEq nicht für den gesamten Fehlertyp implementieren müssen.
running 3 tests
test tests::returns_ok_when_stock_sufficient ... ok
test tests::returns_err_when_insufficient_stock ... ok
test tests::returns_err_when_product_not_found ... ok
9. Häufige Fehler
- Missbrauch von
.unwrap(): Code läuft in der Entwicklung gut, panics unerwartet in der Production bei unerwarteter Eingabe. Verwenden Sie stattdessen?,matchoderunwrap_or_elsemit klarer Behandlung. - Verwendung von
Stringals Fehlertyp in öffentlichen Bibliotheken: Aufrufer können nicht nach Fehlertypmatchen, nur Strings vergleichen – zerbrechlich, wenn Sie die Meldung ändern. Verwenden Sie stattdessen Enum mitthiserror. anyhow::Errorin die öffentliche API der Bibliothek mischen: versteckt spezifische Fehlertyp-Informationen, Aufrufer müssen manuelldowncasten.anyhowsollte nur in der finalen Anwendungsschicht verwendet werden.#[derive(Debug)]vergessen: Der?-Operator und viele Test-Macros erfordern, dass der FehlertypDebugimplementiert. Ohne ihn meldet der Compiler einen Trait-Bound-Fehler.
Hinweis: Wenn Sie aus Java kommen und “überprüfte Exceptions nachahmen” möchten, versuchen Sie nicht, try/catch mit Macros zu replizieren. Gewöhnen Sie sich an die Idee: Fehler sind Werte, die wie jedes andere Datum zurückgegeben und weitergeleitet werden.
10. Fazit
Option<T> und Result<T, E> machen die Behandlung von “fehlendem” und “Fehlschlag” zu einem Teil des Type-Systems, das der Compiler zur Compile-Zeit überprüft, nicht zur Runtime. Der ?-Operator macht Code prägnant und dennoch explizit. Für kleine Projekte reichen String oder anyhow; für gemeinsame Bibliotheken investieren Sie in ein klares Error-Enum mit thiserror, damit Aufrufer jeden Fall sicher behandeln können.