24 days from Node.js to Rust

preface

Rust’s documentation tends to be explanatory and does a poor job of examples, often lumking together unrelated concepts, such as the read_to_string example, which involves SocketAddr and is unfriendly to beginners. In this chapter, we’ll talk about error handling in Rust, which is one of the things you need to know but can’t easily do well

The body of the

Handles multiple error types

Let’s take a look at the failed code at the end of the previous article:

use std::fs::read_to_string;

fn main() - >Result<(), std::io::Error> {
    lethtml = render_markdown()? ;println!("{}", html);
    Ok(())}fn render_markdown() - >Result<String, std::io::Error> {
    let file = std::env::var("MARKDOWN")? ;letsource = read_to_string(file)? ;Ok(markdown::to_html(&source))
}
Copy the code

The problem with this code is the type mismatch. Main () requires that the Error type returned be IO ::Error. We use two? Operator, one of which returns a different type of error. Read_to_string returns the correct Error type Result

but env::var() returns the correct Error type Result
,>
,>

We need a common type to match different kinds of errors, and if you’ve read the previous articles in this tutorial, you’ll know that there are two ways to solve this problem: Traits and enums

Box<dyn Error>

Boxes that implement errors depend on those errors implementing Error traits

Rust must know the size of all data at compile time, but a DYn [trait] value loses its specific type so Rust cannot know its size (see tutorial 10)

You can’t simply return a reference, and Rust won’t let you simply return a reference if the ownership is limited to a function; its lifetime is too short. The Box technique can be thought of as persisting a value, extending its lifetime, and saving a reference to that value so that you can access it

Note: Box does more than that. There are plenty of articles to check out

Now our code looks like this:

use std::{error::Error, fs::read_to_string};

fn main() - >Result< (),Box<dyn Error>> {
    lethtml = render_markdown()? ;println!("{}", html);
    Ok(())}fn render_markdown() - >Result<String.Box<dyn Error>> {
    let file = std::env::var("MARKDOWN")? ;letsource = read_to_string(file)? ;Ok(markdown::to_html(&source))
}
Copy the code

The code now works correctly, but if you read the previous article, Result doesn’t limit Error types, and if the Error in your code doesn’t implement the Error trait, you’ll still have a problem

Method 2: Customize error types

Using dyn [traits] at the cost of losing type information is a way to make your code work, but it’s not a long-term solution

Create your own error type to gain more control. The error type can be struct or enum. The custom error type responsible for multiple types of errors is usually enum:

enum MyError {}
Copy the code

A good Rust citizen implements the Error trait by adding the following code:

impl std::error::Error for MyError {}
Copy the code

VS Code will prompt an error with the above Code, use automatic repair

More prompts will appear asking you to implement the default method

The good news is that this code can be removed, and the default is sufficient. The warning is generated because the Error trait needs to implement Display and Debug:

#[derive(Debug)]
enum MyError {}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Error!") // We have nothing useful to display yet.}}Copy the code

Note: STD :: FMT ::Display can be written as Display. It is written as STD :: FMT ::Display for clarity

Our code as a whole now looks like this:

use std::fs::read_to_string;

fn main() - >Result<(), MyError> {
    lethtml = render_markdown()? ;println!("{}", html);
    Ok(())}fn render_markdown() - >Result<String, MyError> {
    let file = std::env::var("MARKDOWN")? ;letsource = read_to_string(file)? ;Ok(markdown::to_html(&source))
}

#[derive(Debug)]
enum MyError {}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Error!")}}Copy the code

Running the code now exposes two identical errors:? coudn’t convert the error to MyError

[snipped] error[E0277]: `? ` couldn't convert the error to `MyError` --> crates/day-14/custom-error-type/src/main.rs:10:39 | 9 | fn render_markdown() -> Result
      
        { | ----------------------- expected `MyError` because of this 10 | let file = std::env::var("MARKDOWN")? ; | ^ the trait `From
       
        ` is not implemented for `MyError` | = note: the question mark operation (`? `) implicitly performs a conversion on the error value using the `From` trait = note: required because of the requirements on the impl of `FromResidual
        
         >` for `Result
         
          ` note: required by `from_residual` [snipped]
         ,>
        
       
      ,>Copy the code

Just because Rust has a custom Error type does not mean that it knows how to convert other errors to this type. The Error message provides a solution. We need to implement MyError From
<:varerror>
and From< IO ::Error>.

From, Into, TryFrom, TryInto traits

From, Into, TryFrom, and TryInto traits are the basis for magic transformations, and when you look at.into(), you’re actually looking at the results of one or more of the traits mentioned above

The implementation From provides a reverse Into, and TryFrom is a reverse TryInto, with the Try* prefix indicating that the conversion may fail and returning a Result

The implementation of MyError is shown in the code below. Note that we have added a variant of MyError to represent the Error type and that the IOError variant encapsulates the original STD :: IO ::Error

#[derive(Debug)]
enum MyError {
    EnvironmentVariableNotFound,
    IOError(std::io::Error),
}

impl From<std::env::VarError> for MyError {
    fn from(_: std::env::VarError) -> Self {
        Self::EnvironmentVariableNotFound
    }
}

impl From<std::io::Error> for MyError {
    fn from(value: std::io::Error) -> Self {
        Self::IOError(value)
    }
}
Copy the code

The full implementation is as follows, note that in Display we distinguish it with variants:

use std::fs::read_to_string;

fn main() - >Result<(), MyError> {
    lethtml = render_markdown()? ;println!("{}", html);
    Ok(())}fn render_markdown() - >Result<String, MyError> {
    let file = std::env::var("MARKDOWN")? ;letsource = read_to_string(file)? ;Ok(markdown::to_html(&source))
}

#[derive(Debug)]
enum MyError {
    EnvironmentVariableNotFound,
    IOError(std::io::Error),
}

impl From<std::env::VarError> for MyError {
    fn from(_: std::env::VarError) -> Self {
        Self::EnvironmentVariableNotFound
    }
}

impl From<std::io::Error> for MyError {
    fn from(value: std::io::Error) -> Self {
        Self::IOError(value)
    }
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MyError::EnvironmentVariableNotFound => write!(f, "Environment variable not found"),
            MyError::IOError(err) => write!(f, "IO Error: {}", err.to_string()),
        }
    }
}
Copy the code

Method 3: Existing Crate

Any Rust programmer has to deal with errors, and there are plenty of precedents. There’s no need to reinvent the wheel, just use what you already have

thiserror

Thiserror (crates. IO) gives you all the powers of method 2 and is much simpler to use

use std::fs::read_to_string;

fn main() - >Result<(), MyError> {
    lethtml = render_markdown()? ;println!("{}", html);
    Ok(())}fn render_markdown() - >Result<String, MyError> {
    let file = std::env::var("MARKDOWN")? ;letsource = read_to_string(file)? ;Ok(markdown::to_html(&source))
}

#[derive(thiserror::Error, Debug)]
enum MyError {
    #[error("Environment variable not found")]
    EnvironmentVariableNotFound(#[from] std::env::VarError),
    #[error(transparent)]
    IOError(#[from] std::io::Error),
}
Copy the code
error-chain

Error-chain is no longer maintained, but it is really useful for handling errors

Error-chain (crates. IO) gives you a lot of choices and makes it easy to create errors

error_chain::error_chain! {}Copy the code

This line of code gives you an incorrect struct, an enum of the wrong type, and a custom Result type associated with an error. Here is the complete code:

usestd::fs::read_to_string; error_chain::error_chain! { foreign_links { EnvironmentVariableNotFound(::std::env::VarError); IOError(::std::io::Error); }}fn main() - >Result< > () {lethtml = render_markdown()? ;println!("{}", html);
    Ok(())}fn render_markdown() - >Result<String> {
    let file = std::env::var("MARKDOWN")? ;letsource = read_to_string(file)? ;Ok(markdown::to_html(&source))
}
Copy the code
anyhow

The anyhow is another tool developed by the author of ThisError. The difference between the two is that ThisError is more concerned about the specific type of the error, and that the code is designed for the library type; The Anyhow is designed for the application code, and does not care much about the specific error type

reading

  • The Rust Book: ch 9.02 – Recoverable Errors with Result
  • Rust by Example: Multiple Error Types

Related to the library

In addition to the three libraries described in the text, there are many more excellent libraries to help you simplify error handling

  • Snafu (crates.io.)
  • quick-error (crates.io)
  • failure (crates.io)
  • err-derive (crates.io)

conclusion

Rust puts errors first, and once you start respecting them as much as Rust does, you’ll see why robust error handling is one of the most valuable things you can bring back to a JavaScript project. Will you learn how to isolate code that might fail and generate more meaningful error messages and fallbacks

Either thisError or error-chain is a good choice in the code base, and I usually use Anyhow in my test and command line projects. They are high quality options that will greatly address error handling