A concept

How to achieve lightweight error handling in Rust

Unlike programming languages such as C++, C# or Python, Rust relies on explicit error handling for functions that can fail. At first glance, this may seem like an unusual approach, but it was deliberately chosen and has its strengths. How error handling can be implemented in a professional Rust software product.

16.05.2025Text: Urs Häfliger0 Comments
Header Blog Series Rust Error Handling

Error handling is an integral part of any professional software product. Error handling in Rust works differently than in traditional programming languages such as C#, C++ or Python. In these languages, a function that can fail throws an exception. This exception can be handled higher up in the call stack. Rust takes a different approach and highlights potential errors in the function signature, thus forcing explicit error handling.

Specifically, these are recoverable errors, so errors that do not directly cause the program to crash, but can be handled. We can do something about this type of error, even if it just means creating a log entry and terminating the program normally.

Rust Programming Language Quiz
Quiz

Test your knowledge of Rust!

Rust is winning the hearts of developers – perhaps yours too. But how knowledgeable are you about Rust? Very? Then put your knowledge to the test!
To the quiz

Basic building blocks of error handling in Rust

You can tell whether a function can fail by looking at the return type in a function signature:

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

The result can either be the desired type T or an error of type ErrorType.

As users of this function, we examine the result using the match expression:

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

To pass on a possible error, the result does not necessarily have to be explicitly examined, rather the “?” operator can be used:

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

These fundamental error handling building blocks are described in detail in the Rust Book. We illustrate below how these building blocks are combined to create a consistent error handling concept in an application.

Goal of professional error handling

Error handling is an interdisciplinary architectural issue, which requires a concept that should be implemented throughout the entire software. The error handling objectives vary depending on the use case. The following objectives are appropriate in the case of a library that can be used in a broader context.

  • The library only deals with those errors that can be dealt with. To deal with different errors, they must be distinguishable from each other.
  • The only information communicated externally is what went wrong, together with sufficient context.
  • The aim is to create a powerful and solid error handling concept with as little boilerplate code as possible.
A race track with two motorcyclists in the foreground battling it out and other drivers in the background.
Will Rust soon replace C++?

The race between programming languages

C++ has dominated the programming world for decades, but Rust is gaining ground. Find out in this blog post who is likely to win.
Learn more

Multi-layered error handling concept

From this perspective, an architecture exists in accordance with the onion skin principle. This means that outer layers depend on inner layers. The outer layers know the types, functions and methods of the inner layers, but not vice versa.

Innermost layer: domain

To differentiate between errors, it is best to use custom error types. These are implemented by means of enumerations. thiserror is a useful crate for allowing these errors to be passed on conveniently and enriched with context.

The domain contains functions that can fail. This is signalled by corresponding error types and function signatures.

#[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)
    }
}

The error macro (#[error(“…”)]) of the thiserror crate automates implementation of the display and error trait of the error enums. This saves repetitive typing.

If an operation fails, the corresponding error is returned, enriched with the text in the error macro.

rakete_animation_transparent
Rust Transition Service

Securely into the future

Software development is changing rapidly. Our Rust Transition Service is dedicated to making your company’s software secure, efficient and future-proof.
Explore now

Middle layer: use cases

In the use case layer, the errors can be easily passed on with explicit error matching or, if a conversion exists, with the “?” operator.

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

A conversion to a different error type can be automated with the #[from] macro. In the example, a DomainError is packaged in a UseCase::DomainError.

Depending on the error type and situation, an error can be dealt with directly in this layer:

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 the example above, the domain process is called again, but with a modified argument. Other errors, or if the process fails again, are passed on as an error to this layer.

Outermost layer: API

API functions can also fail. It makes sense for the API layer, however, to only return a single error type. This allows the library to be further developed without changing the API. In order to still provide meaningful error messages, the errors can be enriched with context. The anyhow crate is used for this purpose.

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

In this example, the errors from the use case layer are enriched with context and converted to return a uniform result of type anyhow::Result.

Conclusion: Error handling concept for ensuring correct intervention

This concept allows explicit but lightweight error handling to be achieved. Errors can be differentiated and thus addressed at the appropriate level. This is an important building block for writing reliable software.

This Rust Playground provides a working version of a simple but complete error handling concept.

Contact our experts and discuss error handling in your specific product.

The expert

Oliver With

Oliver With is an expert in embedded software. As a senior developer, he is convinced that the best way to solve complex problems is by teams working closely together. He combines creative approaches to finding solutions with high-quality development to create successful products. He is a Rust enthusiast, because for the first time Rust provides a language that combines security, performance, industry acceptance and ergonomics for developers. 

Advanced development made easy

Rust in embedded Linux and Yocto

Linux
Simple handling

Rust: Tools for reliable dependency management

Rust
Prepared for the change

Successfully introducing Rust into your team

Rust

Attention!

Sorry, so far we got only content in German for this section.