Error handling best practices in Rust - Time & Space Complexity
When handling errors in Rust, it's important to know how the program's work grows as it checks and manages errors.
We want to see how error handling affects the time the program takes as input size changes.
Analyze the time complexity of the following Rust code snippet.
fn process_items(items: &[i32]) -> Result {
let mut sum = 0;
for &item in items {
if item < 0 {
return Err("Negative value found".to_string());
}
sum += item;
}
Ok(sum)
}
This code sums a list of numbers but returns an error if any number is negative.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Looping through each item in the list once.
- How many times: Once for each item until a negative is found or all are checked.
The program checks each item one by one. If no negative is found early, it checks all items.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 checks |
| 100 | About 100 checks |
| 1000 | About 1000 checks |
Pattern observation: The work grows roughly in direct proportion to the number of items.
Time Complexity: O(n)
This means the time to handle errors grows linearly with the number of items checked.
[X] Wrong: "Error handling always makes the program slower by a lot."
[OK] Correct: In this example, error checks happen as part of normal looping, so the extra cost is small and grows only with input size.
Understanding how error checks affect program speed helps you write clear and efficient Rust code, a skill valued in many coding challenges and real projects.
"What if we changed the error check to happen after the loop instead of during? How would the time complexity change?"
Practice
Result in Rust error handling?Solution
Step 1: Understand the role of
ResultResultis an enum used to represent either success (Ok) or failure (Err) in Rust operations.Step 2: Identify the purpose in error handling
It helps safely handle errors by forcing the programmer to consider both success and failure cases explicitly.Final Answer:
To represent either success or failure of an operation -> Option BQuick Check:
Result= success or failure [OK]
- Confusing Result with threading or constants
- Thinking Result automatically fixes errors
- Using Result only for numbers or strings
? operator in a function that returns Result<T, E>?Solution
Step 1: Recall the
The?operator usage?operator is placed immediately after a function call that returns aResultto propagate errors easily.Step 2: Check syntax correctness
let value = some_function()?; correctly places?after the function call:some_function()?. Other options misuse the operator placement.Final Answer:
let value = some_function()?; -> Option AQuick Check:
?goes right after function call [OK]
- Placing ? before the function call
- Using ! instead of ?
- Adding ? after semicolon
fn divide(a: i32, b: i32) -> Result {
if b == 0 {
Err(String::from("division by zero"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10, 0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
}Solution
Step 1: Analyze the divide function call
The function is called with b = 0, triggering the error branch returningErr("division by zero").Step 2: Check the match handling in main
TheErrcase prints "Error: division by zero" to the console.Final Answer:
Error: division by zero -> Option CQuick Check:
Error message prints on zero divisor [OK]
- Assuming division returns 0 instead of error
- Expecting panic instead of handled error
- Ignoring match arms
fn read_file() -> Result {
let content = std::fs::read_to_string("file.txt");
Ok(content)
}Solution
Step 1: Understand the return type and function call
The function returnsResult<String, std::io::Error>, butread_to_stringreturns aResultitself.Step 2: Identify missing error propagation
The code assignscontentas aResultbut wraps it again inOkwithout handling errors. The?operator is needed to propagate errors properly.Final Answer:
Missing?operator to propagate error -> Option DQuick Check:
Use ? to handle Result errors [OK]
- Wrapping Result inside Ok again
- Ignoring error propagation
- Changing return type incorrectly
String. Which is the best way to handle errors using Result and the ? operator?
Choose the correct implementation:
fn read_two_files() -> Result {
// Option A
let content1 = std::fs::read_to_string("file1.txt")?;
let content2 = std::fs::read_to_string("file2.txt")?;
Ok(content1 + &content2)
// Option B
let content1 = std::fs::read_to_string("file1.txt").unwrap();
let content2 = std::fs::read_to_string("file2.txt").unwrap();
Ok(content1 + &content2)
// Option C
let content1 = std::fs::read_to_string("file1.txt").expect("file1 missing");
let content2 = std::fs::read_to_string("file2.txt").expect("file2 missing");
Ok(content1 + &content2)
// Option D
let content1 = std::fs::read_to_string("file1.txt");
let content2 = std::fs::read_to_string("file2.txt");
Ok(content1.unwrap() + &content2.unwrap())
}Solution
Step 1: Understand error propagation best practice
Using the?operator propagates errors to the caller without panicking, making the function safe and clear.Step 2: Compare with other options
The other options useunwrap()orexpect(), which cause the program to panic on error, not recommended for robust error handling.Final Answer:
Use?operator to propagate errors cleanly -> Option AQuick Check:
?propagates errors safely [OK]
- Using unwrap or expect causing panics
- Not propagating errors properly
- Ignoring error handling for multiple calls
Ready to prove it?
Pass the test and this pattern moves unknown
