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.
Quiz
Test your knowledge of Rust!
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.
Will Rust soon replace C++?
The race between programming languages
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.
Rust Transition Service
Securely into the future
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.




