[Solved] Getting Started with Rust for the Nth – 8. Error Handling

Article table of contents

  • foreword
  • 8.1 Unrecoverable Error Panic
  • 8.2 Recoverable Error Result

Foreword

This article introduces Rust error handling~

Rust does not have exceptions, but divides error handling into recoverable and unrecoverable errors.

Error Type Description Processing Technique
Recoverable errors usually represent situations where reporting the error to the user and retrying the operation is reasonable, such as file not found Result
Unrecoverable error usually is a synonym for bugs, such as trying to access past the end of the array panic!

8.1 Unrecoverable Error Panic

  • Syntax:
    panic!("Exception information");
    
  • Set the environment variable RUST_BACKTRACE=1 to print the exception stack.
  • Use the panic::catch_unwind function to let the developer catch Panic so that the program can continue without being aborted.
    • Try to avoid panic::catch_unwind as it may cause memory unsafety.
    use std::panic;
    
    fn main() {<!-- -->
        let v = vec![1,2,3];
        println!("{}", v[0]);
        let result = panic::catch_unwind(|| {<!-- --> prinln!("{}", v[99])});
        assert!(result.is_err());
        println!("{}", v[1]);
    }
    
    // 1
    // panic error message
    // 2
    

8.2 Recoverable Error Result

8.2.1 Basic usage

  • Result: Can be used to handle recoverable errors, it uses an enumeration to encapsulate the normal return value and error information.
    enum Result<T, E> {<!-- -->
        Ok(T),
        Err(E),
    }
    
    • T and E are generic type parameters. T represents the type of data in the Ok member returned on success, and E represents the Err returned on failure Wrong type in member.
  • The type Result will be automatically imported by Rust, no need to display the import scope, use Ok(T) or Err(E).
  • The unwrap and expect methods provided by the Result type can implement similar functions of match pattern matching.
    • unwrap(): If no error is returned, continue execution, otherwise throw the error as panic.
    • expect("custom error message"): If no error is returned, continue execution, otherwise the custom error message will be thrown in the form of panic.
  • Propagating errors: Rust provides the ? operator to simplify code for use in functions that return a value of type Result, which is defined to do exactly the same job as match pattern matching Way.
    • If the value of Result is Ok, it will return the value in Ok and continue to execute the following code.
    • If the value of Result is Err, the value in Err will be passed to the caller as the return value of the entire function.
    • The ? operator is passed to the from function, which is defined in the standard library's From trait to convert errors from a type for another type. The error type received by the ? operator is converted to the error type returned by the current function.
  • Combined with the match syntax, recoverable error handling can be performed after receiving a value of type Result:
    use std::fs::File;
    
    fn main() {<!-- -->
        let f = File::open("hello.txt");
        let f = match f {<!-- -->
            Ok(file) => file,
            Err(error) => {<!-- -->
                // Recoverable error handling, return a value as needed or just panic!
                panic!("Problem opening the file: {:?}", error)
            },
        };
    }
    

8.2.2 matches different errors

  • When calling a function in actual use, it is often possible to return more than one type of error, and developers will handle different errors differently.
  • A structure type io::Error is provided in the standard library, and its members correspond to different error types that may be caused by io operations, which can usually be returned as Err members. This structure has a kind() method that returns the value of io::ErrorKind, which will return an enumeration of different errors. Developers can use match expressions to implement different errors. perform different operations.
    use std::fs::File;
    use std::io::ErrorKind;
    
    fn main() {<!-- -->
        let f = File::open("hello.txt");
        let f = match f {<!-- -->
            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),
            },
        };
    }
    
    • It can be seen that if you want to return multiple errors, you can customize an error enumeration and return it as Err.
    • There is also a more concise syntax, which will be introduced later.

8.2.3 Convert recoverable errors to unrecoverable errors: unwrap and expect

unwrap

  • unwrap() of Result:
    • If the value of Result is Ok, the value in Ok will be returned directly.
    • If the value of Result is Err, call the panic! macro to throw the error message in Err.
  • Disadvantage of unwrap: You cannot customize the error message when calling unwrap.
  • unwrap example:
    use std::fs::File;
    fn main() {<!-- -->
        let f = File::open("hello.txt").unwrap();
    }
    

expect

  • The difference between expect and unwrap is that expect can customize the error message.
  • expect("error message") of Result:
    • If the value of Result is Ok, the value in Ok will be returned directly.
    • If the value of Result is Err, call the panic! macro to throw the error message provided by the developer when calling except.
  • The advantage of except: can quickly find the location of the panic through custom information.
  • except example:
    use std::fs::File;
    
    fn main() {<!-- -->
        let f = File::open("hello.txt").expect("Failed to open hello.txt");
    }
    

8.2.4 Propagation errors

Explanation of propagation errors

  • Propagating errors is equivalent to the process of throwing exceptions upwards in java. The difference is that excluding exceptions in java does not distinguish between recoverable and unrecoverable errors.
  • Due to the distinction between recoverable and unrecoverable errors in rust, unrecoverable errors are thrown using panic!, while recoverable errors are given by returning Result . Therefore, the propagation error mentioned here mainly discusses the propagation of recoverable errors. If you want to propagate the error to the upper layer, it is usually repackaged into a new Err after matching the error in the match. returns.
  • Example of propagating errors:
    use std::io;
    use std::io::Read;
    use std::fs::File;
    
    fn read_username_from_file() -> Result<String, io::Error> {<!-- -->
        let f = File::open("hello.txt");
    
        let mut f = match f {<!-- -->
            Ok(file) => file,
            Err(e) => return Err(e), // propagate error 1: pass the error of opening the file to the caller
        };
    
        let mut s = String::new();
    
        match f.read_to_string( &mut s) {<!-- -->
            Ok(_) => Ok(s),
            Err(e) => Err(e), // propagate error 2: pass the error of reading the file to the caller
        }
    }
    
    fn main() {<!-- -->
        let result = read_username_from_file();
    }
    
  • It can be seen that rust actually changes the throw operation in other languages to return Result::Err, thus realizing the unification of returning general values and propagating errors .

? operator

  • ? operator: A shortcut for propagating errors.

  • ? operator usage: Result value?, equivalent to

    // let var_name = Result value?;
    // Equivalent to
    let var_name = match Result value {<!-- -->
        Ok(value) => value,
        Err(e) => return Err(e),
    };
    
    • It can be seen that in many cases, when developers get a Result value, they just want to get the value directly if there is no error. If there is an error, the error is passed to the upper-layer caller, and the upper-layer caller handles it. These are template code template code, so the ? operator is there to simplify writing this one.
  • The role of the ? operator:

    • If Result is Ok: The value in Ok is the result of the expression, and execution continues.
    • If Result is Err: Err is the return value of the entire function, as if return was used.
  • The ? operator can only be applied to functions of type Result.

  • Examples of ? expressions:

    use std::io;
    use std::io::Read;
    use std::fs::File;
    
    fn read_username_from_file() -> Result<String, io::Error> {<!-- --> // equivalent to also telling the caller that an error may occur
        let mut f = File::open("hello.txt")?;
        let mut s = String::new();
        f.read_to_string( &mut s)?;
        Ok(s)
    }
    
  • The chained call form of the above example:

    fn read_username_from_file() -> Result<String, io::Error> {<!-- -->
        let mut s = String::new();
        File::open("hello.txt")?.read_to_string( &mut s)?;
        Ok(s)
    }
    
  • The main function main() can return what type can be the following two:

    • (): return empty
    • Result: returns valid value or error

? and from function

  • Trait std::convert::From has a from function: used to convert between errors.
  • Errors applied by ? are implicitly handled by the from function. The error type received by from will be converted to the error type defined by the return type of the current function.
  • Purpose: For different error reasons, return the same error type.
    • Precondition: Each error type implements a from function that converts to the returned error type.

8.3 When to panic! When to use Result

8.3.1 General principles

  • When defining a function that may fail, return Result in preference, otherwise panic!.
  • Use panic! instead of Result to indicate that the error cannot be recovered and the program must be terminated; use Result instead of panic!, it means that it is possible for the program to be restored and whether or not to restore the decision is up to the caller.
  • Don't be afraid to throw panic!, usually the function will abide by the contract. If the caller does not abide by the contract and causes an unrecoverable error, it is the caller's responsibility, and the program should terminate and let the caller fix the code.

8.3.2 Write examples, prototype code, tests

Scenarios where panic! can be used:

  1. Demonstrate some concepts: eg unwrap.
  2. When writing prototype code: first write the theme structure of the code, and then try the unwrap or expect tags where the details need to be implemented, and add it when the code is improved later. .
  3. When writing test code: You can use unwrap and expect to mark when the test encounters non-compliant returns.

8.3.3 When the developer has more information than the compiler

  • In some scenarios, the developer can determine that Result returns Ok, but when the compiler checks, the developer needs to consider Err situation: At this time, you can directly use unwrap to throw Err.

8.3.4 Guidelines for Error Handling

panic! is best used when the code may end up in a broken state. Bad State: Some assumptions, guarantees, conventions or immutability of the program behavior are broken when the developer writes the logic. For example, an illegal value, a contradictory value, or a missing value is passed into the code, and one of the following:

  • This corrupted state is not something expected to happen.
  • After this, the developer's code cannot run in this corrupted state.
  • There is currently no better way for developers to deal with the current situation.
syntaxbug.com © 2021 All Rights Reserved.