In the previous tutorial, we learned enums and pattern matching. We also saw Option<T> for values that might not exist. Now we learn how Rust handles errors.
Most languages use exceptions — you throw an error and hope someone catches it. Rust does not have exceptions. Instead, Rust uses types to represent errors. The compiler forces you to handle them. No surprise crashes. No unhandled exceptions.
Two Kinds of Errors
Rust splits errors into two categories:
- Unrecoverable errors — bugs that should never happen. Use
panic!. - Recoverable errors — expected failures (file not found, invalid input). Use
Result<T, E>.
Most of your code will use Result. Use panic! only for programming mistakes.
panic! — When Something Is Truly Wrong
panic! immediately stops the program with an error message:
fn main() {
panic!("something went terribly wrong");
}
thread 'main' panicked at 'something went terribly wrong', src/main.rs:2:5
Common situations that panic:
fn main() {
let v = vec![1, 2, 3];
let _x = v[10]; // panic: index out of bounds
// let _n: i32 = "abc".parse().unwrap(); // panic: parse failed
}
panic! is for bugs — things that should be impossible if your code is correct. For expected failures, use Result.
Result<T, E> — The Error Type
Result is an enum with two variants:
enum Result<T, E> {
Ok(T), // Success — contains the value
Err(E), // Failure — contains the error
}
Functions that can fail return Result:
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("cannot divide by zero"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 3.0) {
Ok(result) => println!("Result: {:.2}", result),
Err(error) => println!("Error: {}", error),
}
match divide(10.0, 0.0) {
Ok(result) => println!("Result: {:.2}", result),
Err(error) => println!("Error: {}", error),
}
}
The caller must handle both cases. You cannot accidentally ignore an error.
Option — Missing Values
Option is for values that might not exist. It is not an error — just absence:
fn find_item(items: &[&str], target: &str) -> Option<usize> {
for (i, item) in items.iter().enumerate() {
if *item == target {
return Some(i);
}
}
None
}
fn main() {
let fruits = ["apple", "banana", "cherry"];
match find_item(&fruits, "banana") {
Some(index) => println!("Found at index {}", index),
None => println!("Not found"),
}
}
| Type | Meaning | Variants |
|---|---|---|
Result<T, E> | Operation that can fail | Ok(T) / Err(E) |
Option<T> | Value that might not exist | Some(T) / None |
unwrap and expect
For quick prototyping, unwrap extracts the value or panics:
fn main() {
let number: i32 = "42".parse().unwrap(); // Ok → 42
println!("Parsed: {}", number);
// "abc".parse::<i32>().unwrap(); // Would panic!
}
expect is the same but with a custom message:
fn main() {
let number: i32 = "42".parse()
.expect("failed to parse number");
println!("Parsed: {}", number);
}
Use unwrap/expect only in prototypes, tests, and when you are certain the value is Ok/Some. In real code, handle errors properly.
The ? Operator — Clean Error Propagation
The ? operator is the best way to handle errors in Rust. It unwraps Ok or returns Err early:
use std::num::ParseIntError;
fn parse_and_double(input: &str) -> Result<i32, ParseIntError> {
let number = input.parse::<i32>()?; // If Err, return early
Ok(number * 2)
}
fn main() {
match parse_and_double("21") {
Ok(value) => println!("Result: {}", value), // 42
Err(e) => println!("Error: {}", e),
}
match parse_and_double("abc") {
Ok(value) => println!("Result: {}", value),
Err(e) => println!("Error: {}", e), // Error: invalid digit
}
}
Without ?, you would need to write this:
fn parse_and_double(input: &str) -> Result<i32, ParseIntError> {
let number = match input.parse::<i32>() {
Ok(n) => n,
Err(e) => return Err(e),
};
Ok(number * 2)
}
The ? replaces five lines with one. You can chain multiple ? calls:
fn add_strings(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x = a.parse::<i32>()?;
let y = b.parse::<i32>()?;
Ok(x + y)
}
If any parse fails, the function returns the error immediately.
Custom Error Types
For real applications, you define your own error types:
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
InvalidInput(String),
ParseFailed(ParseIntError),
OutOfRange(i32),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
AppError::ParseFailed(e) => write!(f, "Parse error: {}", e),
AppError::OutOfRange(n) => write!(f, "Value {} is out of range", n),
}
}
}
From Trait — Automatic Error Conversion
The ? operator uses the From trait to convert errors automatically. Implement From on your error type:
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> AppError {
AppError::ParseFailed(e)
}
}
fn parse_age(input: &str) -> Result<i32, AppError> {
let age = input.parse::<i32>()?; // ParseIntError → AppError automatically
if age < 0 || age > 150 {
return Err(AppError::OutOfRange(age));
}
Ok(age)
}
Now when parse returns a ParseIntError, the ? operator calls AppError::from(e) and converts it to your custom type.
Combinators — Functional Error Handling
Result and Option have methods for transforming values without match:
map — Transform the Success Value
fn main() {
let result: Result<i32, String> = Ok(5);
let doubled = result.map(|n| n * 2); // Ok(10)
println!("{:?}", doubled);
}
and_then — Chain Operations That Can Fail
fn parse_positive(input: &str) -> Result<u32, String> {
input.parse::<i32>()
.map_err(|e| e.to_string())
.and_then(|n| {
if n > 0 {
Ok(n as u32)
} else {
Err(String::from("must be positive"))
}
})
}
unwrap_or_else — Default Value on Error
fn main() {
let value = "abc".parse::<i32>()
.unwrap_or_else(|_| 0); // Use 0 if parse fails
println!("Value: {}", value); // 0
}
Common Combinators
| Method | On | Purpose |
|---|---|---|
map | Ok/Some | Transform success value |
map_err | Err | Transform error value |
and_then | Ok/Some | Chain fallible operations |
unwrap_or | Ok/Some | Default value |
unwrap_or_else | Ok/Some | Default from closure |
ok_or | Option | Convert Option to Result |
is_ok / is_some | Both | Check without consuming |
When to Panic vs When to Return Result
| Situation | Use | Why |
|---|---|---|
| Index out of bounds (bug) | panic! | Programming mistake |
| File not found | Result | Expected failure |
| Invalid user input | Result | Expected failure |
| Impossible state reached | panic! | Bug in logic |
| Network timeout | Result | Expected failure |
| Tests and examples | unwrap | Quick and clear |
| Prototype code | unwrap/expect | Speed over safety |
Rule of thumb: If the caller can do something about the error, use Result. If it is a bug that should never happen, use panic!.
A Complete Example
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug, PartialEq)]
enum CalculatorError {
InvalidNumber(String),
DivisionByZero,
}
impl fmt::Display for CalculatorError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CalculatorError::InvalidNumber(s) => write!(f, "Invalid number: {}", s),
CalculatorError::DivisionByZero => write!(f, "Division by zero"),
}
}
}
impl From<ParseIntError> for CalculatorError {
fn from(e: ParseIntError) -> CalculatorError {
CalculatorError::InvalidNumber(e.to_string())
}
}
fn calculate(a: &str, b: &str) -> Result<i32, CalculatorError> {
let x = a.parse::<i32>()?;
let y = b.parse::<i32>()?;
if y == 0 {
return Err(CalculatorError::DivisionByZero);
}
Ok(x / y)
}
fn main() {
match calculate("100", "4") {
Ok(result) => println!("100 / 4 = {}", result),
Err(e) => println!("Error: {}", e),
}
match calculate("100", "0") {
Ok(result) => println!("100 / 0 = {}", result),
Err(e) => println!("Error: {}", e),
}
match calculate("abc", "4") {
Ok(result) => println!("abc / 4 = {}", result),
Err(e) => println!("Error: {}", e),
}
}
Summary
| Concept | Syntax | Purpose |
|---|---|---|
| panic! | panic!("msg") | Unrecoverable error |
| Result | Result<T, E> | Recoverable error |
| Option | Option<T> | Value might not exist |
| ? | value? | Propagate error or unwrap |
| unwrap | .unwrap() | Extract or panic |
| expect | .expect("msg") | Extract or panic with message |
| Custom error | enum MyError { ... } | Application-specific errors |
| From | impl From<E> for MyError | Auto-convert with ? |
| Combinators | .map(), .and_then() | Transform without match |
Source Code
Related Tutorials
- Rust Tutorial #7: Enums and Pattern Matching — enums are the foundation of Result and Option
- Rust Tutorial #6: Structs and Methods — custom types for error modeling
What’s Next?
You now have the tools to write real Rust programs — ownership, borrowing, structs, enums, and error handling. In the next tutorial, we learn traits — how to define shared behavior across types.