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