Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

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,  
        }  
    }  
}

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 (/tmp/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/minigrep`

thread 'main' (469490) panicked at src/main.rs:20: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

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:

#![allow(unused)]
fn main() {
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,  
        }  
    }  
}
}

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 (/tmp/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
     Running `target/debug/minigrep`

thread 'main' (465520) panicked at src/main.rs:21:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

This error message is much better than the previous one.

However, some extra information is still shown, such as thread 'main' (465520) panicked at src/main.rs:21: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):

#![allow(unused)]
fn main() {
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})  
    }  
}
}
  • 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:

#![allow(unused)]
fn main() {
let config = Config::new(&args).unwrap_or_else(|err| {  
    println!("Problem parsing arguments: {}", err);  
    process::exit(1);  
});
}

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' (465520) panicked at src/main.rs:21:13: or backtrace note.

Try it:

$ cargo run
   Compiling minigrep v0.1.0 (/tmp/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
     Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments

The concept of closures will be covered in 13.1. Closures Pt. 1, 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})  
    }  
}