Rust Error Handling â Result, Option & Panic
In this tutorial, you'll learn about Rust Error Handling. We cover key concepts, practical examples, and best practices.
Rust error handling uses the Result and Option types to represent recoverable errors, and panic! for unrecoverable failures, providing type-safe error management without exceptions.
What You'll Learn
In this tutorial, you'll learn how Rust handles errors with Result<T, E> and Option<T>, how the ? operator propagates errors, how to define custom error types, and when to use panic! vs recoverable errors.
Why It Matters
Exception-based error handling in C++ and Java can silently swallow errors or unwind through critical code. Rust's Result type forces you to handle every error at compile time. In systems programming, where a failed file read or network timeout can have cascading consequences, this compile-time guarantee is invaluable.
Real-World Use
File system drivers must handle disk errors gracefully without crashing the kernel. Web servers recover from malformed requests using Result types. Durga Antivirus Pro's scan engine uses Result to handle permission errors, corrupt files, and I/O timeouts without crashing the entire scan pipeline.
flowchart TD
A[Operation] --> B{Can it fail?}
B -->|No| C[Return T]
B -->|Yes, recoverable| D[Return Result]
B -->|Yes, unrecoverable| E[panic!]
D --> F{Caller handles?}
F -->|Yes| G[Match on Ok/Err]
F -->|No| H[Propagate with ?]
H --> I[Up the call stack]
Result and Option Basics
Result<T, E> represents either success (Ok(T)) or failure (Err(E)). Option<T> represents either a value (Some(T)) or absence (None).
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("division by zero"))
} else {
Ok(a / b)
}
}
fn main() {
let result = divide(10.0, 2.0);
match result {
Ok(value) => println!("Result: {}", value),
Err(msg) => println!("Error: {}", msg),
}
let bad = divide(10.0, 0.0);
match bad {
Ok(value) => println!("Result: {}", value),
Err(msg) => println!("Error: {}", msg),
}
}
Expected output:
Result: 5
Error: division by zero
The ? Operator
The ? operator propagates errors to the caller, making code concise without losing type safety.
use std::fs::File;
use std::io::{self, Read};
fn read_file_contents(path: &str) -> io::Result<String> {
let mut file = File::open(path)?; // returns Err immediately if file doesn't exist
let mut contents = String::new();
file.read_to_string(&mut contents)?; // returns Err if read fails
Ok(contents)
}
fn get_file_length(path: &str) -> io::Result<usize> {
let contents = read_file_contents(path)?;
Ok(contents.len())
}
fn main() {
match get_file_length("Cargo.toml") {
Ok(len) => println!("File length: {} bytes", len),
Err(e) => println!("Error reading file: {}", e),
}
}
Expected output:
File length: [number] bytes
Chaining with Combinators
Result and Option provide combinator methods for functional error handling.
fn parse_number(s: &str) -> Option<i32> {
s.parse().ok()
}
fn main() {
// Combinator chaining
let result = parse_number("42")
.map(|n| n * 2)
.filter(|n| *n > 50)
.map(|n| format!("Result: {}", n))
.unwrap_or_else(|| "Failed to process".to_string());
println!("{}", result);
// and_then for fallible chaining
let sum = parse_number("10")
.and_then(|a| parse_number("20").map(|b| a + b));
println!("Sum: {:?}", sum);
// or_else for recovery
let val = parse_number("not_a_number")
.or_else(|| Some(0));
println!("Defaulted: {:?}", val);
}
Expected output:
Result: 84
Sum: Some(30)
Defaulted: Some(0)
Custom Error Types
For larger projects, define custom error types using the thiserror crate or manual implementations.
use std::fmt;
use std::io;
#[derive(Debug)]
enum ScanError {
IoError(io::Error),
ParseError { line: usize, message: String },
Timeout,
}
impl fmt::Display for ScanError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ScanError::IoError(e) => write!(f, "I/O error: {}", e),
ScanError::ParseError { line, message } => {
write!(f, "Parse error at line {}: {}", line, message)
}
ScanError::Timeout => write!(f, "Operation timed out"),
}
}
}
impl From<io::Error> for ScanError {
fn from(e: io::Error) -> ScanError {
ScanError::IoError(e)
}
}
fn scan_file(path: &str) -> Result<String, ScanError> {
let contents = std::fs::read_to_string(path)?; // io::Error -> ScanError via From
if contents.is_empty() {
return Err(ScanError::ParseError {
line: 0,
message: "Empty file".into(),
});
}
Ok(contents)
}
fn main() {
match scan_file("Cargo.toml") {
Ok(contents) => println!("Scanned {} bytes", contents.len()),
Err(e) => println!("Scan failed: {}", e),
}
}
Expected output:
Scanned [number] bytes
When to panic!
Panic is for unrecoverable errors: array index out of bounds, division by zero, assertion failures. In library code, prefer returning Result. In application code, panic at the top level.
fn main() {
// These panic
// let v = vec![1, 2, 3];
// println!("{}", v[10]); // index out of bounds
// Using expect for clear panic messages
let config = std::env::var("CONFIG_PATH")
.expect("CONFIG_PATH environment variable is required");
println!("Config path: {}", config);
}
Error Handling and Security
Proper error handling is a security concern. Leaking internal details in error messages can aid attackers. Rust's error types let you control exactly what information is exposed:
- User-facing errors: Generic messages without internals
- Internal logging: Full error details for debugging
- No stack unwinding in release: Prevent information leaks via panic messages
Durga Antivirus Pro logs full error details internally but returns sanitized messages to the user, preventing information disclosure while enabling debugging.
Common Mistakes
1. Using unwrap() in Production Code
unwrap() panics on error. Use ?, match, or combinators instead. Reserve unwrap() for prototyping and tests.
2. Swallowing Errors with ok()
Calling .ok() on a Result discards the error. Prefer .map_err() or .unwrap_or_else() to handle errors properly.
3. Boxing All Errors
Using Box<dyn Error> for every error type loses type information. Define custom enums for known error cases.
4. Panicking in Library Code
Libraries should return Result, not panic. Panicking affects the caller and may cause unexpected program termination.
5. Ignoring the Error Type's Display Implementation
Custom error types should implement both Display and Debug for proper error reporting.
Practice Questions
1. What is the difference between Result and Option?
Result<T, E> represents success or failure with an error value. Option<T> represents presence or absence of a value. Use Result when failure provides information, Option when absence is the only failure mode.
2. How does the ? operator work?
The ? operator unwraps a Result or Option. If the value is Ok/Some, it unwraps. If Err/None, it returns early from the function, propagating the error.
3. When should you use panic!? Use panic! for unrecoverable errors: out-of-bounds access, assertion failures, invariant violations. Use Result for recoverable errors: file not found, parse failure, network timeout.
4. Why is unwrap() discouraged in production?
unwrap() panics on error, causing program termination. Production code should handle errors gracefully, not crash.
5. Challenge: Create a Config struct that parses key=value pairs from a file, using a custom ConfigError enum with variants for Io, Parse, and DuplicateKey errors.
Mini Project: Safe File Scanner
use std::fs::File;
use std::io::{self, BufRead, BufReader};
#[derive(Debug)]
enum FileScanError {
IoError(io::Error),
InvalidLine { line_number: usize, content: String },
SignatureFound(String),
}
impl From<io::Error> for FileScanError {
fn from(e: io::Error) -> FileScanError {
FileScanError::IoError(e)
}
}
fn scan_file_for_patterns(path: &str, patterns: &[&str]) -> Result<Vec<String>, FileScanError> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut matches = Vec::new();
for (line_num, line) in reader.lines().enumerate() {
let line = line?;
if line.is_empty() {
continue;
}
for pattern in patterns {
if line.contains(pattern) {
matches.push(format!("Line {}: {}", line_num + 1, line.trim()));
}
}
}
Ok(matches)
}
fn main() -> Result<(), FileScanError> {
let matches = scan_file_for_patterns("Cargo.toml", &["rust", "error"])?;
if matches.is_empty() {
println!("No matches found");
} else {
println!("Found {} matches:", matches.len());
for m in matches {
println!(" {}", m);
}
}
Ok(())
}
FAQ
Related Concepts
What's Next
Explore Traits & Generics for polymorphism, and Testing & Documentation for writing robust, tested code.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro