Konzept

Wie Sie in Rust ein leichtgewichtiges Error-Handling erreichen

Rust setzt für Funktionen, die fehlschlagen können, auf explizites Error-Handling – im Gegensatz zu Programmiersprachen wie C++, C# oder Python. Auf den ersten Blick ist dies ein ungewohnter Ansatz, der aber bewusst gewählt wurde und seine Stärken hat. Wie Error-Handling in einem professionellen Rust-Software-Produkt implementiert werden kann.

16.05.2025Text: Urs Häfliger0 Kommentare
Header Blogserie Rust Error-Handling

Error-Handling ist ein integraler Bestandteil jedes professionellen Software-Produkts. In Rust funktioniert das Error-Handling anders als in herkömmlichen Programmiersprachen wie C#, C++ oder Python. In diesen Sprachen wirft eine Funktion, die fehlschlagen kann, eine Exception. Diese Exception kann weiter oben im Call-Stack behandelt werden. Rust verfolgt einen anderen Ansatz und macht mögliche Fehlerfälle in der Funktionssignatur sichtbar und erzwingt damit explizites Error-Handling.

Konkret geht es hier um «recoverable errors», also Fehler, die nicht direkt zum Absturz des Programms führen, sondern behandelbar sind. Bei einem solchen Fehler können wir etwas machen und sei es nur einen Log-Eintrag erstellen und das Programm organisiert beenden.

Rust Prgramming Language Quiz
Quiz

Testen Sie Ihr Rust-Wissen!

Rust erobert die Herzen der Entwickler – vielleicht auch Ihres. Wie fit sind Sie aber auf Rust? Sehr? Dann stellen Sie Ihr Wissen auf die Probe!
Zum Quiz

Fundamentale Bausteine des Error-Handlings in Rust

Man sieht anhand des Rückgabetyps in einer Funktionssignatur, ob eine Funktion fehlschlagen kann:

pub fn fallible_foo<T>(input: &str) -> Result<T, ErrorType>

Das Resultat kann entweder vom gewünschten Typ T sein oder ein Fehler vom Typ ErrorType.

Als Benutzer dieser Funktion untersuchen wir das Result mittels der match Expression:

let result = match fallible_foo("something") {
    Ok(result) => result,
    Err(e) => {
        // Deal with the error or pass it on
        println!("Error: {}", e);
    }
};

Um einen allfälligen Fehler weiterzureichen, muss das Resultat nicht zwingend explizit untersucht werden, sondern der «?»-Operator kann eingesetzt werden:

fn other_fallible_foo() -> Result<String, ErrorType> {
    let result = fallible_foo("something")?;
    //Some logic here
    Ok(result)
}

Diese fundamentalen Bausteine des Error-Handlings sind im Rust-Buch detailliert beschrieben. Im Folgenden soll deutlich werden, wie diese Bausteine zu einem konsistenten Error-Handling-Konzept in einer Applikation kombiniert werden.

Ziel eines professionellen Error-Handlings

Error-Handling ist ein architektonisches Querschnittsthema, wofür es ein Konzept braucht, das in der gesamten Software durchgezogen werden sollte. Je nach Anwendungsfall ist die Zielsetzung für das Error-Handling verschieden. Für eine Library, die in einem grösseren Kontext eingesetzt werden kann, sind folgende Ziele sinnvoll:

  • Innerhalb der Library werden diejenigen Fehler behandelt, die behandelt werden können. Um verschiedene Fehler zu behandeln, müssen sie voneinander unterscheidbar sein.
  • Gegen aussen wird nur mitgeteilt, was schief gegangen ist, zusammen mit genügend Kontext.
  • Angestrebt werden soll ein mächtiges und solides Error-Handling-Konzept mit möglichst wenig Boiler-Plate-Code.
Eine Rennstrecke mit zwei Motorradfahren im Vordergrund, die sich ein Duell liefern, und weiteren Fahrern im Hintergrund
Ersetzt Rust bald C++?

Das Rennen der Programmiersprachen

C++ dominiert seit Jahrzehnten die Programmierwelt, doch Rust ist auf dem Vormarsch. Erfahren Sie in diesem Blogbeitrag, wer gewinnen könnte.
Mehr erfahren

Error-Handling-Konzept mit mehreren Schichten

In dieser Betrachtung existiert eine Architektur nach dem Zwiebelschalenprinzip. Das heisst, dass äussere Schichten von inneren Schichten abhängen. So haben die äusseren Schichten Kenntnis der Typen, Funktionen und Methoden der inneren Schichten, aber nicht umgekehrt.

Innerste Schicht: Domäne

Um Fehler voneinander unterscheiden zu können, liegt die Verwendung von Custom Error Types nahe. Implementiert werden diese mittels Enumerations. Eine nützliche Crate, damit diese Errors komfortabel weitergereicht und mit Kontext angereichter werden können, ist «thiserror».

In der Domäne gibt es Funktionen, die fehlschlagen können. Dies ist signalisiert mit entsprechenden Error-Typen und Funktionssignaturen.

#[derive(Debug, Error)]
pub enum DomainError {
    #[error("Invalid Input {0}")]
    EmptyInput(String),
    #[error("Processing failed")]
    ProcessFailure,
}

pub fn domain_fallible_process(input: &str) -> Result<String, DomainError> {
    if input.is_empty() {
        return Err(DomainError::EmptyInput(format!("Input {} is empty", input)));
    }
    // logic
    if processing_is_ok() {
        Ok(input.to_string())
    } else {
        Err(DomainError::ProcessFailure)
    }
}

Das Error-Macro (#[error(“…”)]), von thiserror automatisiert die Implementation des Display- und Error-Traits der Error-Enums. Dies erspart repetitive Schreibarbeit.

Wenn eine Operation fehlschlägt, wird der entsprechende Error zurückgegeben, angereichert mit dem Text im Error-Macro.

Rust Transition Service

Sicher in die Zukunft

Die Softwareentwicklung verändert sich rasant. Unser Rust Transition Service unterstützt Ihr Unternehmen dabei, Ihre Software sicher, effizient und zukunftsfähig zu gestalten.
Jetzt entdecken

Mittlere Schicht: Use Cases

In der Use-Case-Schicht können die Errors einfach weitergereicht werden mit explizitem Error-Matching oder, falls eine Konversion existiert, mit dem «?»-Operator.

#[derive(Debug, Error)]
pub enum UseCaseError {
    #[error("Domain error: {0}")]
    DomainError(#[from] DomainError),
    #[error("Validation error: {0}")]
    ValidationError(String),
}

Eine Konversion zu einem anderen Error-Typ kann mit dem #[from]-Macro automatisiert werden. Im Beispiel wird ein DomainError in einen UseCase::DomainError verpackt.

Je nach Error-Typ und Situation kann ein Fehler in dieser Schicht direkt behandelt werden:

pub fn usecase_handle_some_errors(input: &str) -> Result<String, UseCaseError> {
    let domain_result = match domain_fallible_process(input) {
        Ok(r) => r,
        Err(DomainError::EmptyInput(_)) => {
            let improved_input = format!("sensible_improvement_{}", input);
            domain_fallible_process(&improved_input)?
        }
        Err(e) => return Err(UseCaseError::from(e)),
    };

    // logic
    Ok(format!("Use Case processed: '{}'", domain_result))
}

In obigem Beispiel wird der Domänenprozess noch einmal aufgerufen, aber mit modifiziertem Argument. Andere Fehler oder wenn der Prozess ein weiteres Mal fehlschlägt, werden als Error dieser Schicht weitergereicht.

Äusserste Schicht: API

Auch API-Funktionen können fehlschlagen. Für die API-Schicht ist es aber sinnvoll, nur einen einzigen Fehlertyp zurückzugeben. So kann die Library weiterentwickelt werden, ohne die API zu ändern. Um trotzdem aussagekräftige Fehlermeldungen zur Verfügung zu stellen, können die Errors mit Kontext angereichter werden. Dazu dient die Crate «anyhow».

pub fn api_fallible_operation(input: &str) -> anyhow::Result<String> {
    let use_case_result = usecase_passon_errors(input)
        .with_context(|| format!("Failed processsing Input {}", input))?;
    // logic
    Ok(format!("API processed: {}", use_case_result))
}

In diesem Beispiel werden die Fehler aus der Use-Case-Schicht mit Kontext angereichert und konvertiert, sodass ein uniformes Resultat vom Typ anyhow::Result zurückgegeben wird.

Fazit: Error-Handling-Konzept zur richtigen Intervention

Mit diesem Konzept wird ein explizites, aber leichtgewichtiges Error-Handling erreicht. Das Konzept erlaubt es, Fehler zu unterscheiden und damit auf geeigneter Stufe zu intervenieren. Dies ist ein wichtiger Baustein, um zuverlässige Software zu schreiben.

In diesem Rust Playground gibt es eine lauffähige Version eines einfachen, aber kompletten Error-Handling-Konzepts.

Nehmen Sie mit unseren Experten Kontakt auf und diskutieren das Error-Handling in Ihrem spezifischen Produkt.

Der Experte

Oliver With

Oliver With ist Spezialist für Embedded Software. Als Senior-Entwickler ist er überzeugt, dass eng zusammenarbeitende Teams komplexe Probleme am besten lösen. Kreativität in der Lösungsfindung vereint er mit Qualität in der Entwicklung, um erfolgreiche Produkte zu schaffen. Er ist Rust-Enthusiast, weil es mit Rust erstmals eine Sprache gibt, die Sicherheit, Performance, Akzeptanz in der Industrie und Ergonomie für Entwickler kombiniert. 

Einfach in der Handhabung

Rust: Werkzeuge für ein sicheres Dependency Management

Rust
Vorbereitet für den Change

Erfolgreiche Einführung von Rust in Ihrem Team

Rust
Die wichtigsten IT-Trends für Schweizer KMU 2025

«Nicht alle Trends schaffen es in den bbv Technica Radar»

Digitalisierung

Beachtung!

Entschuldigung, bisher haben wir nur Inhalte in English für diesen Abschnitt.