|
| 1 | +# 12.4 Refactoring Pt.2 - Error Handling |
| 2 | + |
| 3 | +## 12.4.0 Before We Begin |
| 4 | +In Chapter 12, we will build a real project: a command-line program. This program is a `grep` (**Global Regular Expression Print**), a tool for global regular-expression search and output. Its job is to **search for the specified text in the specified file.** |
| 5 | + |
| 6 | +This project has several steps: |
| 7 | +- [Receive command-line arguments](../12.1/12.1._Receiving_Command-Line_Arguments.md) |
| 8 | +- [Read files](../12.2/12.2._Read_Files.md) |
| 9 | +- Refactor: improve modules and error handling (this article) |
| 10 | +- Use TDD (test-driven development) to develop library functionality |
| 11 | +- Use environment variables |
| 12 | +- Write error messages to standard error instead of standard output |
| 13 | + |
| 14 | +## 12.4.1 Review |
| 15 | +In the previous section, to improve modularity, we created a struct for the variables and moved the argument-parsing function into a method on that struct. Here is all the code written up to the previous article: |
| 16 | +```rust |
| 17 | +use std::env; |
| 18 | +use std::fs; |
| 19 | + |
| 20 | +struct Config { |
| 21 | + query: String, |
| 22 | + filename: String, |
| 23 | +} |
| 24 | + |
| 25 | +fn main() { |
| 26 | + let args:Vec<String> = env::args().collect(); |
| 27 | + let config = Config::new(&args); |
| 28 | + |
| 29 | + let contents = fs::read_to_string(config.filename) |
| 30 | + .expect("Something went wrong while reading the file"); |
| 31 | + println!("With text:\n{}", contents); |
| 32 | +} |
| 33 | + |
| 34 | +impl Config { |
| 35 | + fn new(args: &[String]) -> Config { |
| 36 | + let query = args[1].clone(); |
| 37 | + let filename = args[2].clone(); |
| 38 | + Config { |
| 39 | + query, |
| 40 | + filename, |
| 41 | + } |
| 42 | + } |
| 43 | +} |
| 44 | +``` |
| 45 | + |
| 46 | +## 12.4.2 Unexpected Input |
| 47 | +The program works correctly only when the user provides valid input. Let’s try running it without arguments: |
| 48 | +``` |
| 49 | +$ cargo run |
| 50 | + Compiling minigrep v0.1.0 (file:///projects/minigrep) |
| 51 | + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s |
| 52 | + Running `target/debug/minigrep` |
| 53 | +thread 'main' panicked at src/main.rs:27:21: |
| 54 | +index out of bounds: the len is 1 but the index is 1 |
| 55 | +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace |
| 56 | +``` |
| 57 | +It reports `Index out of bounds`. As the programmer, we know this is because there were not enough arguments, so the program went out of bounds when using an index to fetch them. But a user cannot understand this error message, so they cannot correct the mistake. |
| 58 | + |
| 59 | +What this article will do is make the program’s error messages easier to understand. |
| 60 | + |
| 61 | +## 12.4.3 Specifying Error Messages |
| 62 | +The way to help users understand the error is to provide our own error message. In the previous example, the panic happened when `Config::new` tried to access an out-of-bounds index, so let’s modify that part: |
| 63 | +```rust |
| 64 | +impl Config { |
| 65 | + fn new(args: &[String]) -> Config { |
| 66 | + if args.len() < 3 { |
| 67 | + panic!("Not enough arguments"); |
| 68 | + } |
| 69 | + let query = args[1].clone(); |
| 70 | + let filename = args[2].clone(); |
| 71 | + Config { |
| 72 | + query, |
| 73 | + filename, |
| 74 | + } |
| 75 | + } |
| 76 | +} |
| 77 | +``` |
| 78 | +If `args` contains fewer than three elements, panic and print `"Not enough arguments"` to tell the user that too few arguments were provided. |
| 79 | + |
| 80 | +Try running it without arguments again: |
| 81 | +``` |
| 82 | +$ cargo run |
| 83 | + Compiling minigrep v0.1.0 (file:///projects/minigrep) |
| 84 | + Finished `dev` profile [unoptimized +debuginfo] target(s) in 0.0s |
| 85 | + Running `target/debug/minigrep` |
| 86 | +thread 'main' panicked at src/main.rs:26:13: |
| 87 | +not enough arguments |
| 88 | +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace |
| 89 | +``` |
| 90 | +This error message is much better than the previous one. |
| 91 | + |
| 92 | +However, some extra information is still shown, such as `thread 'main' panicked at src/main.rs:26:13:` and `note: run with RUST_BACKTRACE=1 environment variable to display a backtrace`. Those are for programmers, not for users, so they should be removed too. |
| 93 | + |
| 94 | +## 12.4.4 Using the `Result` Type |
| 95 | +`panic!` is appropriate when the program itself has a bug. But here, the problem is that the program was used incorrectly because there were too few arguments. For this kind of problem, using `Result` to propagate the error is the best choice (see [9.2. Result Enum and Recoverable Errors Pt. 1](../../Chapter-09/9.2/9.2._Result_Enum_and_Recoverable_Errors_Pt._1_-_Match,_Expect,_and_Unwrap_Handling_Errors.md) and [Pt. 2](../../Chapter-09/9.3/9.3._Result_Enum_and_Recoverable_Errors_Pt._2_-_Error_Propagation,_Question_Mark_Operator,_and_Chained_Calls.md) for details): |
| 96 | +```rust |
| 97 | +impl Config { |
| 98 | + fn new(args: &[String]) -> Result<Config, &'static str> { |
| 99 | + if args.len() < 3 { |
| 100 | + return Err("Not enough arguments"); |
| 101 | + } |
| 102 | + let query = args[1].clone(); |
| 103 | + let filename = args[2].clone(); |
| 104 | + Ok(Config { query, filename}) |
| 105 | + } |
| 106 | +} |
| 107 | +``` |
| 108 | +- Error information must be wrapped in `Err`, and successful return values must be wrapped in `Ok`. |
| 109 | +- The `Ok` variant of `Result` returns a `Config` instance, while `Err` returns an `&str` string literal. However, the compiler does not know where that `&str` comes from or how long its lifetime is, so we need a lifetime annotation. We want it to remain valid for the entire program run, so we write it as `&'static str`, the static lifetime. |
| 110 | + |
| 111 | +Since the return type of `new` has changed, the code in `main` that receives its value must also change: |
| 112 | +```rust |
| 113 | +let config = Config::new(&args).unwrap_or_else(|err| { |
| 114 | + println!("Problem parsing arguments: {}", err); |
| 115 | + process::exit(1); |
| 116 | +}); |
| 117 | +``` |
| 118 | +The `unwrap_or_else` method accepts a `Result`. If it is `Ok`, it returns the value inside `Ok`, similar to `unwrap`. If it is `Err`, the method calls a closure. |
| 119 | + |
| 120 | +A closure is an anonymous function that we define and pass as an argument to `unwrap_or_else`. Its syntax uses two pipes `||`, with a variable name in the middle as the parameter. Here it is `err`, which can be used inside the closure body, such as when printing the error. |
| 121 | + |
| 122 | +Then we use `process::exit` from the standard library. Remember to import it first with `use std::process;`. Calling `exit` terminates the program immediately, and its argument, `1` in the example, becomes the program’s exit status code. This means that after `println!("Problem parsing arguments: {}", err);`, the program stops, so there is no `thread 'main' panicked at src/main.rs:26:13:` or backtrace note. |
| 123 | + |
| 124 | +Try it: |
| 125 | +``` |
| 126 | +$ cargo run |
| 127 | + Compiling minigrep v0.1.0 (file:///projects/minigrep) |
| 128 | + Finished `dev` profile [unoptimized +debuginfo] target(s) in 0.48s |
| 129 | + Running `target/debug/minigrep` |
| 130 | +Problem parsing arguments: not enough arguments |
| 131 | +``` |
| 132 | + |
| 133 | +**The concept of closures will be covered in the next chapter, so it is fine if this does not make complete sense yet; a rough understanding is enough here.** |
| 134 | + |
| 135 | +## 12.4.5 The Full Code |
| 136 | +Here is all the code written up to this article: |
| 137 | +```rust |
| 138 | +use std::env; |
| 139 | +use std::fs; |
| 140 | +use std::process; |
| 141 | + |
| 142 | +struct Config { |
| 143 | + query: String, |
| 144 | + filename: String, |
| 145 | +} |
| 146 | + |
| 147 | +fn main() { |
| 148 | + let args:Vec<String> = env::args().collect(); |
| 149 | + let config = Config::new(&args).unwrap_or_else(|err| { |
| 150 | + println!("Problem parsing arguments: {}", err); |
| 151 | + process::exit(1); |
| 152 | + }); |
| 153 | + |
| 154 | + let contents = fs::read_to_string(config.filename) |
| 155 | + .expect("Something went wrong while reading the file"); |
| 156 | + println!("With text:\n{}", contents); |
| 157 | +} |
| 158 | + |
| 159 | +impl Config { |
| 160 | + fn new(args: &[String]) -> Result<Config, &'static str> { |
| 161 | + if args.len() < 3 { |
| 162 | + return Err("Not enough arguments"); |
| 163 | + } |
| 164 | + let query = args[1].clone(); |
| 165 | + let filename = args[2].clone(); |
| 166 | + Ok(Config { query, filename}) |
| 167 | + } |
| 168 | +} |
| 169 | +``` |
0 commit comments