Error Handling
Error Handling
Summary
- rust errors are explicit
- recoverable and non-recoverable
- types are known ahead of time (transparent)
- work well with rust data types (struct/enum)
- syntactic sugar built made it easier
Explicit
Explicit is better than implicit. – python
import this
Rust errors are explicit.
Errors are explicit? Nani
Example
In python help page
class dict(object)
|
| __getitem__(...)
| x.__getitem__(y) <==> x[y]
Does it raise exceptions?
Types of errors
- recoverable errors
- unrecoverable errors
Unrecoverable errors
Unexpected - panic!
, out of bounds
fn main() {
let v = vec![1, 2, 3];
v[99];
}
________________
____/ ( ( ) ) \___
/( ( ( ) _ )) ) )\
(( ( )( ) ) ( ) )
((/ ( _( ) ( _) ) ( () ) )
( ( ( (_) (( ( ) .((_ ) . )_
( ( ) ( ( ) ) ) . ) ( )
( ( ( ( ) ( _ ( _) ). ) . ) ) ( )
( ( ( ) ( ) ( )) ) _)( ) ) )
( ( ( \ ) ( (_ ( ) ( ) ) ) ) )) ( )
( ( ( ( (_ ( ) ( _ ) ) ( ) ) )
( ( ( ( ( ) (_ ) ) ) _) ) _( ( )
(( ( )( ( _ ) _) _(_ ( (_ )
(_((__(_(__(( ( ( | ) ) ) )_))__))_)___)
((__) \\||lll|l||/// \_))
( /(/ ( ) ) )\ )
( ( ( ( | | ) ) )\ )
( /(| / ( )) ) ) )) )
( ( ((((_(|)_))))) )
( ||\(|(|)|/|| )
( |(||(||)|||| )
( //|/l|||)|\\ \ )
(/ / // /|//||||\\ \ \ \ _)
-------------------------------------------------------------------------------
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
0: rust_begin_unwind
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14
2: core::panicking::panic_bounds_check
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9
6: panic::main
at ./src/main.rs:4:5
7: core::ops::function::FnOnce::call_once
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
When to use non-recoverable errors?
- non-recoverable
- memory errors
- index errors
- impossible errors,
unwrap()
->expect()
- exception, kernel
Useful parts
- prototype code
- examples
- tests
Recoverable errors
-
Result
sum typesenum Result<T, E> { Ok(T), Err(E), }
Ok
for successErr
for failure
Example error
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
}
File::open
returns a Result<File, io::Error>
.
Handling result (including error)
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {:?}", error),
};
}
When failed to open file:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Error, who cares?
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
// Err(error) => panic!("Problem opening the file: {:?}", error),
};
}
Rust cares
- due to exhaustive pattern matching and sum types
error[E0004]: non-exhaustive patterns: `Err(_)` not covered
--> src/main.rs:6:31
|
6 | let greeting_file = match greeting_file_result {
| ^^^^^^^^^^^^^^^^^^^^ pattern `Err(_)` not covered
|
note: `Result<File, std::io::Error>` defined here
--> /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/core/src/result.rs:502:1
::: /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/core/src/result.rs:511:5
|
= note: not covered
= note: the matched value is of type `Result<File, std::io::Error>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
7 ~ Ok(file) => file,
8 ~ Err(_) => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `playground` (bin "playground") due to previous error
Matching different errors
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => {
panic!("Problem opening the file: {:?}", other_error);
}
},
};
}
Shortcuts
unwrap
- used for no-brainer error handling, good for prototyping-
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt").unwrap(); }
-
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:49
-
expect
- like unwrap but with message, more readable, use this if possible-
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt") .expect("hello.txt should be included in this project"); }
-
thread 'main' panicked at 'hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:10
-
Propagating errors
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
Using the ?
operator
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
Or chaining method calls
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
Syntactic sugar ?
fn read_username_from_file() -> Result<String, io::Error> {
let file = File::open("hello.txt")?;
...
Can imagine as
fn read_username_from_file() -> Result<String, io::Error> {
let file = match File::open("hello.txt") {
Ok(f) => f,
Err(e) => return Err(e),
};
...
Patterns - custom error types
struct ConsumeError(u64); // can contains data
fn consume(n: u64) -> Result<u64, ConsumeError> {
if n < 10 {
Ok(n)
} else {
Err(ConsumeError(n))
}
}
Patterns - wrapping (external) errors
use std::fs::File;
struct ConsumeError(u64); // can contains data
fn consume(n: u64) -> Result<u64, ConsumeError> {
if n < 10 {
Ok(n)
} else {
Err(ConsumeError(n))
}
}
enum ProcessError {
IoError(std::io::Error),
ParseError(std::num::ParseIntError),
ConsumeError(ConsumeError),
}
fn process(f: &str) -> Result<u64, ProcessError> {
let s = std::fs::read_to_string(f).map_err(ProcessError::IoError)?;
let n = s.parse().map_err(ProcessError::ParseError)?;
let n = consume(n).map_err(ProcessError::ConsumeError)?;
Ok(n)
}
Alternatives
Option<T>
-Some<T>
andNone
(nullable)thiserror
crate - mainly for library codeanyhow
crate - mainly for application code
References
- https://doc.rust-lang.org/book/ch09-00-error-handling.html