Introduction: The Philosophy of Error Handling in Rust
Unlike many languages with exceptions (Java, Python, C++), Rust does not use the classic try/catch construct. Instead, the language offers a more explicit and type-safe approach. Errors in Rust are not hidden control flows, but return values. This makes code predictable and protects against "silent" failures.
Rust has two main types for representing error situations:
- Result<T, E> — for recoverable errors (I/O errors, parsing errors, business logic errors).
- Option<T> — for the absence of a value (an analogue of null, but safe).
In this article, we will break down how to work with these types, what to replace them with from the world of try/catch, and how to write reliable code in Rust.
1. Result<T, E>: The Main Tool for Error Handling
The Result type is an enum with two variants: Ok(T) (success) and Err(E) (error). It is used in most functions that might fail.
1.1. Creating and Returning a Result
use std::fs::File;use std::io::Read;
fn read_file(path: &str) -> Result<String, std::io::Error> { let mut file = File::open(path)?; // the ? operator — analogue of try! let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents)}Here, the ? operator is a key element. It works like try in other languages: if the result is Err, the function immediately returns that error. If Ok, it extracts the value.
1.2. Handling with match
The most explicit way is to use match:
let result = read_file("config.txt");
match result { Ok(content) => println!("File read: {}", content), Err(e) => eprintln!("Error: {}", e),}This is an analogue of try/catch, but more flexible and controlled.
1.3. Helper Methods
.unwrap()— panics on error (only for prototypes)..expect(msg)— panics with a custom message..unwrap_or(default)— returns a default value..is_ok()/.is_err()— state checking.
2. Option<T>: Safe Handling of Absent Values
Option is another enum with variants Some(T) and None. It replaces null and nullptr, eliminating null pointer dereference errors.
2.1. Usage Example
fn find_user(id: u32) -> Option<String> { if id == 1 { Some("Alice".to_string()) } else { None }}
let user = find_user(2);println!("User: {}", user.unwrap_or("Guest".to_string()));2.2. Combining with Result
Often Option is converted to Result for uniform handling:
fn get_user_name(id: u32) -> Result<String, String> { find_user(id).ok_or("User not found".to_string())}The .ok_or() method turns Some into Ok, and None into Err.
3. The ? Operator: An Elegant Replacement for try/catch
The ? operator is syntactic sugar that automatically unwraps Result or Option. It is only available in functions that return the corresponding type.
3.1. Example with a Call Chain
use std::num::ParseIntError;
fn parse_and_add(a: &str, b: &str) -> Result<i32, ParseIntError> { let x: i32 = a.parse()?; let y: i32 = b.parse()?; Ok(x