|
| 1 | +% Error Handling in Rust |
| 2 | + |
| 3 | +> The best-laid plans of mice and men |
| 4 | +> Often go awry |
| 5 | +> |
| 6 | +> "Tae a Moose", Robert Burns |
| 7 | +
|
| 8 | +Sometimes, things just go wrong. It's important to have a plan for when the |
| 9 | +inevitable happens. Rust has rich support for handling errors that may (let's |
| 10 | +be honest: will) occur in your programs. |
| 11 | + |
| 12 | +There are two main kinds of errors that can occur in your programs: failures, |
| 13 | +and panics. Let's talk about the difference between the two, and then discuss |
| 14 | +how to handle each. Then, we'll discuss upgrading failures to panics. |
| 15 | + |
| 16 | +# Failure vs. Panic |
| 17 | + |
| 18 | +Rust uses two terms to differentiate between two forms of error: failure, and |
| 19 | +panic. A **failure** is an error that can be recovered from in some way. A |
| 20 | +**panic** is an error that cannot be recovered from. |
| 21 | + |
| 22 | +What do we mean by 'recover'? Well, in most cases, the possibility of an error |
| 23 | +is expected. For example, consider the `from_str` function: |
| 24 | + |
| 25 | +```{rust,ignore} |
| 26 | +from_str("5"); |
| 27 | +``` |
| 28 | + |
| 29 | +This function takes a string argument and converts it into another type. But |
| 30 | +because it's a string, you can't be sure that the conversion actually works. |
| 31 | +For example, what should this convert to? |
| 32 | + |
| 33 | +```{rust,ignore} |
| 34 | +from_str("hello5world"); |
| 35 | +``` |
| 36 | + |
| 37 | +This won't work. So we know that this function will only work properly for some |
| 38 | +inputs. It's expected behavior. We call this kind of error 'failure.' |
| 39 | + |
| 40 | +On the other hand, sometimes, there are errors that are unexpected, or which |
| 41 | +we cannot recover from. A classic example is an `assert!`: |
| 42 | + |
| 43 | +```{rust,ignore} |
| 44 | +assert!(x == 5); |
| 45 | +``` |
| 46 | + |
| 47 | +We use `assert!` to declare that something is true. If it's not true, something |
| 48 | +is very wrong. Wrong enough that we can't continue with things in the current |
| 49 | +state. Another example is using the `unreachable!()` macro |
| 50 | + |
| 51 | +```{rust,ignore} |
| 52 | +enum Event { |
| 53 | + NewRelease, |
| 54 | +} |
| 55 | +
|
| 56 | +fn probability(_: &Event) -> f64 { |
| 57 | + // real implementation would be more complex, of course |
| 58 | + 0.95 |
| 59 | +} |
| 60 | +
|
| 61 | +fn descriptive_probability(event: Event) -> &'static str { |
| 62 | + match probability(&event) { |
| 63 | + 1.00 => "certain", |
| 64 | + 0.00 => "impossible", |
| 65 | + 0.00 ... 0.25 => "very unlikely", |
| 66 | + 0.25 ... 0.50 => "unlikely", |
| 67 | + 0.50 ... 0.75 => "likely", |
| 68 | + 0.75 ... 1.00 => "very likely", |
| 69 | + } |
| 70 | +} |
| 71 | +
|
| 72 | +fn main() { |
| 73 | + std::io::println(descriptive_probability(NewRelease)); |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +This will give us an error: |
| 78 | + |
| 79 | +```{notrust,ignore} |
| 80 | +error: non-exhaustive patterns: `_` not covered [E0004] |
| 81 | +``` |
| 82 | + |
| 83 | +While we know that we've covered all possible cases, Rust can't tell. It |
| 84 | +doesn't know that probability is between 0.0 and 1.0. So we add another case: |
| 85 | + |
| 86 | +```rust |
| 87 | +use Event::NewRelease; |
| 88 | + |
| 89 | +enum Event { |
| 90 | + NewRelease, |
| 91 | +} |
| 92 | + |
| 93 | +fn probability(_: &Event) -> f64 { |
| 94 | + // real implementation would be more complex, of course |
| 95 | + 0.95 |
| 96 | +} |
| 97 | + |
| 98 | +fn descriptive_probability(event: Event) -> &'static str { |
| 99 | + match probability(&event) { |
| 100 | + 1.00 => "certain", |
| 101 | + 0.00 => "impossible", |
| 102 | + 0.00 ... 0.25 => "very unlikely", |
| 103 | + 0.25 ... 0.50 => "unlikely", |
| 104 | + 0.50 ... 0.75 => "likely", |
| 105 | + 0.75 ... 1.00 => "very likely", |
| 106 | + _ => unreachable!() |
| 107 | + } |
| 108 | +} |
| 109 | + |
| 110 | +fn main() { |
| 111 | + println!("{}", descriptive_probability(NewRelease)); |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +We shouldn't ever hit the `_` case, so we use the `unreachable!()` macro to |
| 116 | +indicate this. `unreachable!()` gives a different kind of error than `Result`. |
| 117 | +Rust calls these sorts of errors 'panics.' |
| 118 | + |
| 119 | +# Handling errors with `Option` and `Result` |
| 120 | + |
| 121 | +The simplest way to indicate that a function may fail is to use the `Option<T>` |
| 122 | +type. Remember our `from_str()` example? Here's its type signature: |
| 123 | + |
| 124 | +```{rust,ignore} |
| 125 | +pub fn from_str<A: FromStr>(s: &str) -> Option<A> |
| 126 | +``` |
| 127 | + |
| 128 | +`from_str()` returns an `Option<A>`. If the conversion succeeds, it will return |
| 129 | +`Some(value)`, and if it fails, it will return `None`. |
| 130 | + |
| 131 | +This is appropriate for the simplest of cases, but doesn't give us a lot of |
| 132 | +information in the failure case. What if we wanted to know _why_ the conversion |
| 133 | +failed? For this, we can use the `Result<T, E>` type. It looks like this: |
| 134 | + |
| 135 | +```rust |
| 136 | +enum Result<T, E> { |
| 137 | + Ok(T), |
| 138 | + Err(E) |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +This enum is provided by Rust itself, so you don't need to define it to use it |
| 143 | +in your code. The `Ok(T)` variant represents a success, and the `Err(E)` variant |
| 144 | +represents a failure. Returning a `Result` instead of an `Option` is recommended |
| 145 | +for all but the most trivial of situations. |
| 146 | + |
| 147 | +Here's an example of using `Result`: |
| 148 | + |
| 149 | +```rust |
| 150 | +#[deriving(Show)] |
| 151 | +enum Version { Version1, Version2 } |
| 152 | + |
| 153 | +#[deriving(Show)] |
| 154 | +enum ParseError { InvalidHeaderLength, InvalidVersion } |
| 155 | + |
| 156 | +fn parse_version(header: &[u8]) -> Result<Version, ParseError> { |
| 157 | + if header.len() < 1 { |
| 158 | + return Err(ParseError::InvalidHeaderLength); |
| 159 | + } |
| 160 | + match header[0] { |
| 161 | + 1 => Ok(Version::Version1), |
| 162 | + 2 => Ok(Version::Version2), |
| 163 | + _ => Err(ParseError::InvalidVersion) |
| 164 | + } |
| 165 | +} |
| 166 | + |
| 167 | +let version = parse_version(&[1, 2, 3, 4]); |
| 168 | +match version { |
| 169 | + Ok(v) => { |
| 170 | + println!("working with version: {}", v); |
| 171 | + } |
| 172 | + Err(e) => { |
| 173 | + println!("error parsing header: {}", e); |
| 174 | + } |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +This function makes use of an enum, `ParseError`, to enumerate the various |
| 179 | +errors that can occur. |
| 180 | + |
| 181 | +# Non-recoverable errors with `panic!` |
| 182 | + |
| 183 | +In the case of an error that is unexpected and not recoverable, the `panic!` |
| 184 | +macro will induce a panic. This will crash the current task, and give an error: |
| 185 | + |
| 186 | +```{rust,ignore} |
| 187 | +panic!("boom"); |
| 188 | +``` |
| 189 | + |
| 190 | +gives |
| 191 | + |
| 192 | +```{notrust,ignore} |
| 193 | +task '<main>' panicked at 'boom', hello.rs:2 |
| 194 | +``` |
| 195 | + |
| 196 | +when you run it. |
| 197 | + |
| 198 | +Because these kinds of situations are relatively rare, use panics sparingly. |
| 199 | + |
| 200 | +# Upgrading failures to panics |
| 201 | + |
| 202 | +In certain circumstances, even though a function may fail, we may want to treat |
| 203 | +it as a panic instead. For example, `io::stdin().read_line()` returns an |
| 204 | +`IoResult<String>`, a form of `Result`, when there is an error reading the |
| 205 | +line. This allows us to handle and possibly recover from this sort of error. |
| 206 | + |
| 207 | +If we don't want to handle this error, and would rather just abort the program, |
| 208 | +we can use the `unwrap()` method: |
| 209 | + |
| 210 | +```{rust,ignore} |
| 211 | +io::stdin().read_line().unwrap(); |
| 212 | +``` |
| 213 | + |
| 214 | +`unwrap()` will `panic!` if the `Option` is `None`. This basically says "Give |
| 215 | +me the value, and if something goes wrong, just crash." This is less reliable |
| 216 | +than matching the error and attempting to recover, but is also significantly |
| 217 | +shorter. Sometimes, just crashing is appropriate. |
| 218 | + |
| 219 | +There's another way of doing this that's a bit nicer than `unwrap()`: |
| 220 | + |
| 221 | +```{rust,ignore} |
| 222 | +let input = io::stdin().read_line() |
| 223 | + .ok() |
| 224 | + .expect("Failed to read line"); |
| 225 | +``` |
| 226 | +`ok()` converts the `IoResult` into an `Option`, and `expect()` does the same |
| 227 | +thing as `unwrap()`, but takes a message. This message is passed along to the |
| 228 | +underlying `panic!`, providing a better error message if the code errors. |
0 commit comments