DEV Community

Cover image for [Rust Guide] 12.4. Refactoring Pt.2 - Error Handling
SomeB1oody
SomeB1oody

Posted on

[Rust Guide] 12.4. Refactoring Pt.2 - Error Handling

12.4.0 Before We Begin

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.

This project has several steps:

  • Receive command-line arguments
  • Read files
  • Refactor: improve modules and error handling (this article)
  • Use TDD (test-driven development) to develop library functionality
  • Use environment variables
  • Write error messages to standard error instead of standard output

If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.

12.4.1 Review

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:

use std::env;  
use std::fs;  

struct Config {  
    query: String,  
    filename: String,  
}  

fn main() {  
    let args:Vec<String> = env::args().collect();  
    let config = Config::new(&args);  

    let contents = fs::read_to_string(config.filename)  
        .expect("Something went wrong while reading the file");  
    println!("With text:\n{}", contents);  
}  

impl Config {  
    fn new(args: &[String]) -> Config {  
        let query = args[1].clone();  
        let filename = args[2].clone();  
        Config {  
            query,  
            filename,  
        }  
    }  
}
Enter fullscreen mode Exit fullscreen mode

12.4.2 Unexpected Input

The program works correctly only when the user provides valid input. Let’s try running it without arguments:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Enter fullscreen mode Exit fullscreen mode

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.

What this article will do is make the program’s error messages easier to understand.

12.4.3 Specifying Error Messages

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:

impl Config {  
    fn new(args: &[String]) -> Config {  
        if args.len() < 3 {  
            panic!("Not enough arguments");  
        }  
        let query = args[1].clone();  
        let filename = args[2].clone();  
        Config {  
            query,  
            filename,  
        }  
    }  
}
Enter fullscreen mode Exit fullscreen mode

If args contains fewer than three elements, panic and print "Not enough arguments" to tell the user that too few arguments were provided.

Try running it without arguments again:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized +debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:26:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Enter fullscreen mode Exit fullscreen mode

This error message is much better than the previous one.

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.

12.4.4 Using the Result Type

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 and Pt. 2 for details):

impl Config {  
    fn new(args: &[String]) -> Result<Config, &'static str> {  
        if args.len() < 3 {  
            return Err("Not enough arguments");  
        }  
        let query = args[1].clone();  
        let filename = args[2].clone();  
        Ok(Config { query, filename})  
    }  
}
Enter fullscreen mode Exit fullscreen mode
  • Error information must be wrapped in Err, and successful return values must be wrapped in Ok.
  • 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.

Since the return type of new has changed, the code in main that receives its value must also change:

let config = Config::new(&args).unwrap_or_else(|err| {  
    println!("Problem parsing arguments: {}", err);  
    process::exit(1);  
});
Enter fullscreen mode Exit fullscreen mode

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.

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.

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.

Try it:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized +debuginfo] target(s) in 0.48s
     Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments
Enter fullscreen mode Exit fullscreen mode

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.

12.4.5 The Full Code

Here is all the code written up to this article:

use std::env;  
use std::fs;  
use std::process;  

struct Config {  
    query: String,  
    filename: String,  
}  

fn main() {  
    let args:Vec<String> = env::args().collect();  
    let config = Config::new(&args).unwrap_or_else(|err| {  
        println!("Problem parsing arguments: {}", err);  
        process::exit(1);  
    });  

    let contents = fs::read_to_string(config.filename)  
        .expect("Something went wrong while reading the file");  
    println!("With text:\n{}", contents);  
}  

impl Config {  
    fn new(args: &[String]) -> Result<Config, &'static str> {  
        if args.len() < 3 {  
            return Err("Not enough arguments");  
        }  
        let query = args[1].clone();  
        let filename = args[2].clone();  
        Ok(Config { query, filename})  
    }  
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)