Error Handling¶
This module introduces Rust's explicit approach to missing values and
recoverable errors. Instead of using null values or exceptions for ordinary
control flow, Rust programs commonly use Option<T> and Result<T, E>.
The main example extends the matrix type from earlier modules with checked indexing. The goal is to make out-of-bounds access visible in the type signature instead of silently producing invalid behavior.
Learning Objectives¶
After completing this module, participants should be able to:
- Use
Option<T>to represent a value that may be absent. - Use
Result<T, E>to represent an operation that may fail. - Explain the difference between
Some,None,Ok, andErr. - Convert an
Optioninto aResultwithok_or_else. - Transform the value inside an
Optionwithmap. - Use the
?operator to return early from a failing operation. - Handle errors at the call site with
expectwhen failure indicates a bug. - Decide whether a function should return
OptionorResult. - Recognize when panicking is less appropriate than returning an error.
Prerequisites¶
Participants should already be comfortable with:
- Defining structs and methods.
- Shared and mutable references.
- Basic enum syntax.
- Closures.
- Matrix indexing concepts from the structs and traits modules.
The main example used in this module is:
Missing Values With Option¶
Option<T> represents either a value of type T or no value:
Some(value)
None
The matrix example uses Option<usize> for checked index calculation:
fn index(&self, row: usize, col: usize) -> Option<usize> {
if row < self.rows && col < self.cols {
Some(row * self.cols + col)
} else {
None
}
}
This function returns Some(flat_index) when the row and column are valid. It
returns None when the requested element is outside the matrix.
Run the example with:
cd source-code/error-handling
cargo run -- --help
Transforming Option With map¶
The get method uses the checked index to return a matrix value:
pub fn get(&self, row: usize, col: usize) -> Option<f64> {
self.index(row, col).map(|index| self.data[index])
}
The map call transforms the value inside Some:
- If
index(row, col)returnsSome(index),mapapplies the closure and returnsSome(self.data[index]). - If
index(row, col)returnsNone,mapleaves it asNone.
This avoids a manual match for a simple transformation.
The return type communicates the possibility of absence:
Option<f64>
A caller cannot ignore that possibility accidentally.
Recoverable Failure With Result¶
Result<T, E> represents either success or failure:
Ok(value)
Err(error)
The matrix set method can fail if the requested index is out of bounds:
pub fn set(&mut self, row: usize, col: usize, value: f64) -> Result<(), String> {
let index = self
.index(row, col)
.ok_or_else(|| format!("matrix index ({row}, {col}) is out of bounds"))?;
self.data[index] = value;
Ok(())
}
The success type is () because a successful set operation does not need to
return a meaningful value. It only needs to signal that the mutation succeeded.
The error type is String because the example returns a human-readable error
message.
Converting Option To Result¶
The index helper returns Option<usize>, but set wants to return a
Result<(), String>. The conversion happens here:
.ok_or_else(|| format!("matrix index ({row}, {col}) is out of bounds"))?
ok_or_else converts:
Some(index)intoOk(index);NoneintoErr(message).
The closure:
|| format!("matrix index ({row}, {col}) is out of bounds")
constructs the error message only when it is needed.
Use this pattern when absence is detected by one helper function, but the public operation should report a recoverable error with context.
The ? Operator¶
The ? operator propagates an error from the current function:
let index = self
.index(row, col)
.ok_or_else(|| format!("matrix index ({row}, {col}) is out of bounds"))?;
If the expression before ? is Ok(index), the index value is extracted and
execution continues.
If it is Err(error), the current function returns early with that error.
This keeps the successful path readable while still handling failure
explicitly. Without ?, the same code would need a match:
let index = match self
.index(row, col)
.ok_or_else(|| format!("matrix index ({row}, {col}) is out of bounds"))
{
Ok(index) => index,
Err(error) => return Err(error),
};
The ? operator is one of the main reasons Rust error-handling code can remain
compact without hiding the fact that a function can fail.
Handling Errors At The Call Site¶
The example fills the matrix using loop indices that are known to be in bounds:
for i in 0..matrix.rows() {
for j in 0..matrix.cols() {
matrix
.set(i, j, (i * matrix.cols() + j) as f64)
.expect("loop indices should be in bounds");
}
}
The call to expect says: if this operation fails, stop the program and print
this message.
That is acceptable here because failure would indicate a programming mistake in
the loop bounds. The loops use 0..matrix.rows() and 0..matrix.cols(), so
the indices should be valid.
The same pattern appears when reading values back:
let value = matrix.get(i, j).expect("loop indices should be in bounds");
Use expect when failure would mean the programmer's assumptions are wrong.
For ordinary user input or file-system errors, returning or reporting the error
is usually better than panicking.
Option Or Result?¶
Use Option<T> when absence is expected and no additional explanation is
needed:
fn index(&self, row: usize, col: usize) -> Option<usize>
The caller only needs to know whether the index exists.
Use Result<T, E> when failure should carry information:
fn set(&mut self, row: usize, col: usize, value: f64) -> Result<(), String>
Here, an out-of-bounds write is a failed operation and the error message can explain what went wrong.
As examples grow, Result becomes especially important for:
- opening files;
- reading data;
- parsing input;
- validating command-line parameters;
- writing output;
- reporting invalid configuration.
Panics Versus Recoverable Errors¶
Rust also has panics. A panic stops normal execution, usually because the program has reached a state it cannot sensibly recover from.
Panics are appropriate for programming errors and violated internal
assumptions. Recoverable errors are better represented with Result.
In this repository, both styles appear for teaching purposes:
- checked methods such as
getandsetreturnOptionorResult; - indexing syntax in the trait example panics for out-of-bounds access, similar to Rust slices;
expectis used where the example knows loop indices should be valid.
The practical habit is to make expected failure explicit and reserve panics for bugs or unrecoverable internal assumptions.
Toward Fallible Programs¶
Many real command-line programs have a fallible main function:
fn main() -> Result<(), Box<dyn std::error::Error>> {
// fallible work
Ok(())
}
This allows ? to be used in main when opening files, reading data, parsing
input, or writing output.
The iterator module already uses this style when reading CSV data:
fn main() -> Result<(), Box<dyn Error>> {
let mut reader = csv::Reader::from_path(args.file)?;
for result in reader.deserialize() {
let value: Values = result?;
// use value
}
Ok(())
}
The exact error type can be refined later. At this stage, the important point
is that errors are part of the function signature and can be propagated with
?.
Suggested Hands-On Work¶
Use this sequence as a practical lab.
- Run the matrix error-handling example:
bash
cd source-code/error-handling
cargo run -- --rows 3 --cols 4
-
Open
source-code/error-handling/src/matrix.rsand identify the methods that returnOptionandResult. -
Change the
getmethod locally to use a manualmatchinstead ofmap. -
Add a call in
main.rsthat tries to read an out-of-bounds element and handles theNonecase withmatch. -
Add a call in
main.rsthat tries to write an out-of-bounds element and prints the error message instead of usingexpect. -
Temporarily remove the
?fromsetand rewrite the error propagation with a manualmatch. -
Change the error message in
ok_or_elseto include the valid matrix shape. -
Compare the API of
getwith the API ofset: discuss why one returnsOption<f64>and the other returnsResult<(), String>.
Discussion Points¶
This module is a good place to emphasize:
OptionandResultmake uncertainty visible in types.- Absence and failure are related but not identical concepts.
mapis useful when transforming a successfulOptionvalue.ok_or_elseis useful when converting absence into a meaningful error.?keeps the successful path readable while still propagating errors.expectis best reserved for cases where failure indicates a bug.- Fallible
mainfunctions are common in programs that do I/O.
Connection To Later Modules¶
Error handling becomes more important as examples grow:
- Project-organization examples use shared functions that should report failures consistently.
- Randomness and data-generation examples involve file creation and output.
- Julia set examples read command-line arguments or TOML configuration files.
- The N-body example writes CSV output and handles optional output files.
Once participants are comfortable with Option, Result, and ?, they are
ready to study how larger Cargo packages organize shared code and tests.