Error Handling
Error handling is an essential aspect of any programming language, and Rust is no exception.
Rust provides robust error handling mechanisms, like the Result
type and the ?
operator, which allow you to deal with errors in a clean and idiomatic way.
And you can also bail out for unrecoverable errors with panics.
The Result Type
Rust has a built-in Result
enum for handling errors in a type-safe manner.
The Result
type is defined as follows:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
The Result
enum has two variants:
Ok
, which represents a successful operation and contains the result valueErr
, which represents a failed operation and contains the error value
The Result
type encourages you to handle errors explicitly and provides a clear separation between the success and error cases.
This is one of the things that makes Rust so powerful for writing robust code:
You can't forget to handle errors, the type system will require that you address them.
You can use pattern matching to handle the different variants of the Result
type:
fn main() { let result = some_function(); match result { Ok(value) => println!("Success: {}", value), Err(error) => println!("Error: {}", error), } } fn some_function() -> Result<String, String> { // ... }
You can also use unwrap
or expect
if you know the returned value is an Ok.
But you have to be very sure: if you unwrap an Err, your program will panic!
This is okay in instances like missing configuration where you don't want the program to run at all if it isn't there, but you have to be deliberate.
As a general rule, don't use unwrap
or expect
unless you want the program to crash if it fails.
The ? Operator
While pattern matching on Result
values is powerful, it can become verbose when dealing with multiple operations that may return errors.
To simplify error handling in these situations, Rust provides the ?
operator.
It's only usable when you're expecting to return a Result value, and in those cases is tremendously helpful.
The ?
operator, when placed after an expression that returns a Result
, automatically handles the error case.
If the expression returns an Err
, the function immediately returns the error value.
If the expression returns an Ok
, the ?
operator unwraps the Ok
value and continues executing the function.
Here's an example of using the ?
operator:
fn main() { match read_config() { Ok(config) => println!("Config: {:?}", config), Err(error) => println!("Error: {}", error), } } fn read_config() -> Result<String, String> { let file = read_file("config.txt")?; let parsed = parse_config(&file)?; Ok(parsed) } fn read_file(path: &str) -> Result<String, String> { // ... } fn parse_config(file_contents: &str) -> Result<String, String> { // ... }
In this example, the read_config
function calls two other functions that return Result
values.
Instead of using pattern matching to handle the errors, the ?
operator is used to simplify the code.
It elevates the "happy path" so that you can read through that directly, and you know that errors will trickle up.
Panics
Rust has another mechanism for error handling called panics. A panic is a runtime error that results in the immediate termination of the program. Panics are reserved for exceptional situations where it's impossible or undesirable to continue executing the program, such as an unrecoverable error or a broken invariant.
You can cause a panic explicitly using the panic!
macro:
fn main() { let index = 10; if index > 5 { panic!("Index is out of bounds!"); } // ... }
When a panic occurs, Rust unwinds the stack, running destructors for all objects in scope and then terminating the program. Alternatively, Rust can be configured to abort the program directly, without unwinding the stack.
Panics should be used sparingly and only in exceptional circumstances.
In most cases, you should prefer using the Result
type for error handling, as it promotes explicit and robust error handling.