Intermediate · Error handling and collections · topic 23 of 50 · 30 min
Result and the ? operator
Explanation
Result: errors as values
Many languages use exceptions: an error "jumps" out of a function, and you hope someone catches it. In Rust, a function that can fail says so in its return type. Result is a normal enum from the standard library:
enum Result<T, E> {
Ok(T), // success, holding the value
Err(E), // failure, holding the error
}Because the error is part of the type, you cannot forget it. You cannot use a Result<u32, ...> as a u32; you must look inside first. Also, if you call a function that returns a Result and ignore it completely, the compiler warns: "unused Result that must be used".
Handling a Result with match
use std::fs;
fn main() {
match fs::read_to_string("prices.txt") {
Ok(text) => println!("Read {} bytes", text.len()),
Err(e) => println!("Could not read prices.txt: {e}"),
}
}Writing your own fallible function
fn parse_age(text: &str) -> Result<u8, String> {
let age: u8 = match text.trim().parse() {
Ok(n) => n,
Err(_) => return Err(format!("'{text}' is not a number")),
};
if age < 18 {
return Err(String::from("must be 18 or older"));
}
Ok(age)
}The ? operator
Writing match for every call gets long. The question mark operator ? does the common thing for you: if the Result is Ok, it unwraps the value; if it is Err, it returns that error from the current function immediately. It only works inside a function that itself returns a Result (or an Option).
use std::fs;
use std::io;
fn read_first_line(path: &str) -> Result<String, io::Error> {
let text = fs::read_to_string(path)?; // on error: return Err(e) right here
let first = text.lines().next().unwrap_or("").to_string();
Ok(first)
}The ? also converts the error type when needed. If your function returns Result<T, MyError> and the call gives a different error type, ? uses the From trait to convert it, if a conversion exists. You will build such conversions in the next topic.
main can return a Result
use std::error::Error;
use std::fs;
fn main() -> Result<(), Box<dyn Error>> {
let text = fs::read_to_string("orders.csv")?;
let count = text.lines().count();
println!("{count} orders");
Ok(())
}Handy Result and Option helpers
| Helper | What it does |
|---|---|
| map_err(f) | Change the error into another type or message |
| ok_or(err) | Turn an Option into a Result: None becomes Err(err) |
| unwrap_or(default) | The value, or a default on Err |
| is_ok() / is_err() | Check without taking the value |
| ok() | Turn a Result into an Option, dropping the error |
fn find_price(item: &str) -> Option<u32> {
if item == "chai" { Some(15) } else { None }
}
fn bill(item: &str, qty: &str) -> Result<u32, String> {
let price = find_price(item).ok_or(format!("unknown item {item}"))?;
let qty: u32 = qty.parse().map_err(|_| format!("bad quantity {qty}"))?;
Ok(price * qty)
}Common mistakes
- Using ? in a function that returns () (like a plain main). Error: "the ? operator can only be used in a function that returns Result or Option". Change the return type.
- Forgetting to wrap the final value: returning age instead of Ok(age).
- Mixing error types so ? cannot convert them. Use map_err, a common error enum, or Box<dyn Error>.
- Silencing a Result with let _ = ...; when the error actually matters.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Result and the ? operator · 1/6
In Rust, errors are values. A function that can fail returns a Result: Ok with the value, or Err with the error.
Prefer a video by a teacher? Find free videos on “rust result error handling question mark operator” on YouTube ↗
In real life: Validating a KYC form field
A fintech app checks a PAN number typed during KYC (Know Your Customer). The check returns Result<String, String>: Ok with the cleaned number, or Err with a message to show next to the field. The screen code uses match to show either a green tick or the error. Nothing crashes, and every failure has a clear reason.
fn check_pan(input: &str) -> Result<String, String> {
let pan = input.trim().to_uppercase();
if pan.len() != 10 {
return Err(String::from("PAN must have 10 characters"));
}
let ok = pan.chars().enumerate().all(|(i, c)| match i {
0..=4 | 9 => c.is_ascii_uppercase(),
_ => c.is_ascii_digit(),
});
if ok { Ok(pan) } else { Err(String::from("PAN format is ABCDE1234F")) }
}
fn main() {
for input in ["abcde1234f", "ABC12"] {
match check_pan(input) {
Ok(p) => println!("valid: {p}"),
Err(e) => println!("error: {e}"),
}
}
}Practice (0/3 done)
What does this print? fn half(n: i32) -> Result<i32, String> { if n % 2 != 0 { return Err(format!("{n} is odd")); } Ok(n / 2) } fn main() { println!("{:?} {:?}", half(8), half(5)); }
Why does this not compile? fn main() { let n: u32 = "12".parse()?; println!("{n}"); }
Write fn total_from_file(path: &str) -> Result<u32, Box<dyn std::error::Error>> that reads a file with one price per line and returns the sum. Use ? for reading the file and for parsing each line. Test it with a good file, a missing file and a file containing the word "ten".
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Custom error typesError handling and collections · same module
- Vec<T>: growable listsError handling and collections · same module
- Operators and type coercionJavaScript Programming (language in depth) · First steps: values, variables and operators
- Functions and multiple return valuesGo Programming (backend and cloud) · Control flow and functions
- Errors as values: wrap, Is and AsGo Programming (backend and cloud) · Pointers, interfaces and errors
- Functions: declare, define, call and returnC++ Programming (modern C++ and DSA) · Decisions, loops and functions
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What are the two variants of Result?
Answer: Ok and Err
Result<T, E> is either Ok(T) or Err(E).
What does the ? operator do on an Err?
Answer: Returns the error from the current function
? propagates the error to the caller.
Where can you use ? ?
Answer: Only in functions that return Result or Option
The function must have a return type that can carry the early return.
What does Some(3).ok_or("none") give?
Answer: Ok(3)
ok_or turns Some(v) into Ok(v) and None into Err.
Why does the compiler warn about an unused Result?
Answer: Ignoring it may hide an error
Result is marked must_use so errors are not silently dropped.
What can an enum variant hold in Rust?Related
Answer: Any data: none, a tuple of values, or named fields
Rust variants can be unit-like, tuple-like or struct-like.
From Enums and Option
Which trait gives an error a human-readable message for {}?Related
How does a Go function usually report failure?Related
Answer: By returning an error value as the last result
Go functions return an error as the last result; nil means success.
Which verb in fmt.Errorf wraps an error so errors.Is can find it?Related
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 3 more topics in “Error handling and collections” before its exam: Custom error types, Vec<T>: growable lists, HashMap and HashSet.
- Module 6 exam: Error handling and collections · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Modules, crates, traits and generics. 50 topics left in the course before the final exam.