📜 ⬆️ ⬇️

Error Handling in Rust

Like many programming languages, Rust encourages the developer to handle errors in a certain way. In general, there are two general error handling approaches: using exceptions and via return values. And Rust prefers return values.



In this article we intend to detail the work with errors in Rust. Moreover, we will try to immerse ourselves in error handling from various angles time after time, so that at the end you will have a confident practical idea of ​​how all this fits together.



In a naive implementation, error handling in Rust can look verbose and annoying. We will consider the main stumbling blocks, as well as demonstrate how to make error handling concise and convenient using the standard library.



Content


This article is very long, mainly because we start from the very beginning - considering the types of sums (sum type) and combinators, and then try to consistently explain the Rust approach to error handling. So developers who have experience with other expressive type systems can freely jump from section to section.


')

The basics


Error handling can be viewed as a varied analysis of whether some calculation was performed successfully or not. As will be shown later, the key to the convenience of error handling is to reduce the amount of explicit variable analysis that the developer must perform, while keeping the code easily compatible with other code (composability).



(Translator's note: Variable analysis is one of the most commonly used methods of analytical thinking, which is to consider a problem, question, or some situation from the point of view of each possible specific case. At the same time, considering each such case separately is sufficient to solve the initial question.



An important aspect of this approach to solving problems is that such an analysis should be exhaustive. In other words, when using a variance analysis, all possible cases should be considered.



In Rust, variable analysis is implemented using the syntactic construction of match . At the same time, the compiler guarantees that such analysis will be exhaustive: if the developer does not consider all possible options for the specified value, the program will not be compiled.)



Preserving the compatibility of the code is important, because without this requirement we could just get panic whenever we come across something unexpected. ( panic causes the current thread to interrupt and, in most cases, terminates the entire program.) Here is an example:



 //     1  10. //     ,   ,  true. //     false. fn guess(n: i32) -> bool { if n < 1 || n > 10 { panic!(" : {}", n); } n == 5 } fn main() { guess(11); } 

If you try to run this code, the program will crash with a message like this:



 thread '<main>' panicked at ' : 11', src/bin/panic-simple.rs:6 

Here is another, less contrived example. A program that takes a number as an argument doubles its value and prints on the screen.



 use std::env; fn main() { let mut argv = env::args(); let arg: String = argv.nth(1).unwrap(); //  1 let n: i32 = arg.parse().unwrap(); //  2 println!("{}", 2 * n); } 

If you run this program without parameters (error 1) or if the first parameter is not an integer number (error 2), the program will end in panic, just as in the first example.



Handling errors in a similar style is like an elephant in a china shop. The elephant will rush in the direction in which he wants, and destroy everything in its path.



Explanation unwrap


In the previous example, we stated that the program would simply panic if one of the two conditions for the occurrence of an error is fulfilled, although, unlike the first example, there is no explicit panic call in the program code. However, the panic call is built into the unwrap call.



unwrap in Rust is like saying: “Give me the result of the calculations, and if an error occurs, just panic and stop the program.” We could just show the source code of the unwrap function, because it’s pretty simple, but before that we have to deal with the types Option and Result . Both of these types have an unwrap method defined for them.



Option type


Option type is declared in the standard library :



 enum Option<T> { None, Some(T), } 

Option type is a way to express the possibility of the absence of anything using the Rust type system. Expression of the possibility of absence through a type system is an important concept, since such an approach allows the compiler to require the developer to handle such an absence. Let's take a look at an example that tries to find a character in a string:



 //  Unicode- `needle`  `haystack`.    , //      .   `None`. fn find(haystack: &str, needle: char) -> Option<usize> { for (offset, c) in haystack.char_indices() { if c == needle { return Some(offset); } } None } 

Note that when this function finds the corresponding character, it returns not just offset . Instead, it returns Some(offset) . Some is a variant or constructor of a value for type Option . It can be interpreted as a function of type fn<T>(value: T) -> Option<T> . Accordingly, None is also a value constructor, only it has no parameters. It can be interpreted as a function of type fn<T>() -> Option<T> .



It may seem that we made a lot of noise out of nothing, but this is only half the story. The second half is the use of the find function we wrote. Let's try to use it to find the extension in the file name.



 fn main() { let file_name = "foobar.rs"; match find(file_name, '.') { None => println!("   ."), Some(i) => println!(" : {}", &file_name[i+1..]), } } 

This code uses pattern matching to perform variable analysis for the Option<usize> value returned by the find function. In fact, variable analysis is the only way to get to the value stored inside Option<T> . This means that you, as a developer, are required to handle the case when the Option<T> value is None , not Some(t) .



But wait, what about the unwrap we used ? There was no variable analysis! Instead, the variable analysis was moved inside the unwrap method. You can do it yourself if you want:



 enum Option<T> { None, Some(T), } impl<T> Option<T> { fn unwrap(self) -> T { match self { Option::Some(val) => val, Option::None => panic!("called `Option::unwrap()` on a `None` value"), } } } 

The unwrap method abstracts variable analysis . This is exactly what makes unwrap convenient to use. Sorry, panic! means that unwrap inconvenient to combine with another code: it is an elephant in a china shop.



Combination of values Option<T>


In the previous example, we looked at how you can use find to get the file name extension. Of course, not all file names can be found . , so there is a possibility that the name of some file does not have an extension. This absence feature is interpreted at the type level through the use of Option<T> . In other words, the compiler will force us to consider the possibility that the extension does not exist. In our case, we just type a message about it.



Getting the file name extension is a fairly common operation, so it makes sense to put the code into a separate function:



 //     ,    , //     `.`   . //   `file_name`     `.`,  `None`. fn extension_explicit(file_name: &str) -> Option<&str> { match find(file_name, '.') { None => None, Some(i) => Some(&file_name[i+1..]), } } 

(Hint: do not use this code. Instead, use the extension method from the standard library.)



The code looks simple, but its important aspect is that the find function makes us consider the probability of missing values. This is good, because it means that the compiler will not allow us to accidentally forget about the option when the extension is missing in the file name. On the other hand, every time performing an explicitly variable analysis, just as we did in extension_explicit , can become a bit tedious.



In fact, variable analysis in extension_explicit is a very common pattern: if Option<T> has a certain T value, then convert it using a function, and if not, just return None .



Rust supports parametric polymorphism, so it is very easy to declare a combinator that abstracts this behavior:



 fn map<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> A { match option { None => None, Some(value) => Some(f(value)), } } 

In fact, the map defined in the standard library as the Option<T> method.



Armed with our new combinator, we can rewrite our extension_explicit method to get rid of the variative analysis:



 //     ,    , //     `.`   . //   `file_name`     `.`,  `None`. fn extension(file_name: &str) -> Option<&str> { find(file_name, '.').map(|i| &file_name[i+1..]) } 

There is one more behavior that can often be met - this is the use of the default value in the case where the Option value is None . For example, your program may consider that the file extension is rs if it is actually missing.



It is easy to imagine that this case of alternative analysis is not specific only to file extensions - this approach can work with any Option<T> :



 fn unwrap_or<T>(option: Option<T>, default: T) -> T { match option { None => default, Some(value) => value, } } 

The trick is that the default value should be of the same type as the value that can be inside Option<T> . Using this method is elementary:



 fn main() { assert_eq!(extension("foobar.csv").unwrap_or("rs"), "csv"); assert_eq!(extension("foobar").unwrap_or("rs"), "rs"); } 

(Note that unwrap_or declared as an Option<T> method in the standard library, so we used it instead of the function we declared earlier. Don't forget to also study the more general unwrap_or_else method).



There is one more combinator that we think is worth paying special attention to: and_then . It makes it easy to combine various calculations that allow for the possibility of absence . An example is most of the code in this section that is associated with the definition of the extension of a given file name. To do this, we first need to know the name of the file, which is usually extracted from the file path . Although most file paths contain a file name, this is not the case with all file paths. An example is the way . .. or / .



Thus, we have defined the task of finding the extension of a given file path . Let's start with an explicit variable analysis:



 fn file_path_ext_explicit(file_path: &str) -> Option<&str> { match file_name(file_path) { None => None, Some(name) => match extension(name) { None => None, Some(ext) => Some(ext), } } } fn file_name(file_path: &str) -> Option<&str> { unimplemented!() //   } 

You might think we could just use a map combinator to reduce variable analysis, but its type does not quite fit. The fact is that map accepts a function that only does something with an internal value. The result of such a function always Some . Instead, we need a method similar to map , but which allows the caller to pass another Option . Its general implementation is even simpler than the map :



 fn and_then<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> Option<A> { match option { None => None, Some(value) => f(value), } } 

Now we can rewrite our function file_path_ext without explicit variable analysis:



 fn file_path_ext(file_path: &str) -> Option<&str> { file_name(file_path).and_then(extension) } 

Option type has many other combinators defined in the standard library . It is very useful to review this list and familiarize yourself with the available methods - they will often help you reduce the number of variable analysis. Familiarization with these combinators will pay off also because many of them are defined with similar semantics for the Result type, which we will discuss later.



Combinators make use of types like Option more convenient, because they reduce the explicit variable analysis. They also meet the compatibility requirements, as they allow the caller to handle the possibility of no result in their own way. Methods such as unwrap it impossible, because they will panic when Option<T> is None .



Result type


The type Result also defined in the standard library :



 enum Result<T, E> { Ok(T), Err(E), } 

Type Result is an advanced version of Option . Instead of expressing the possibility of absence , as Option does, Result expresses the possibility of error . As a rule, errors are necessary to explain why the result of a particular calculation was not obtained. Strictly speaking, this is a more general form of Option . Consider the following type alias, which in all senses is semantically equivalent to the real Option<T> :



 type Option<T> = Result<T, ()>; 

Here, the second parameter of the Result type Result fixed and defined by () (pronounced as “unit” or “empty tuple”). Type () has exactly one value - () . (Yes, this is the type and value of this type, which look the same!)



The Result type is a way to express one of two possible outcomes of a calculation. By convention, one outcome means the expected result or " Ok ", while the other outcome means an exceptional situation or " Err ".



Like Option , the Result type has an unwrap method defined in the standard library . Let's announce it by ourselves:



 impl<T, E: ::std::fmt::Debug> Result<T, E> { fn unwrap(self) -> T { match self { Result::Ok(val) => val, Result::Err(err) => panic!("called `Result::unwrap()` on an `Err` value: {:?}", err), } } } 

This is actually the same as the Option::unwrap , except that we added the error value to the panic! message panic! . This makes debugging easier, but it forces us to require the parameter type E (which represents our type of error) to implement the Debug . Since the vast majority of types must implement Debug , usually in practice this restriction does not interfere. (Implementing a Debug for some type simply means that there is a reasonable way to print a readable description of the value of this type.)



OK, let's go to the example.



Convert string to number


The standard library Rust allows elementary to convert strings to integers. In fact, it is so simple that it is tempting to write something like:



 fn double_number(number_str: &str) -> i32 { 2 * number_str.parse::<i32>().unwrap() } fn main() { let n: i32 = double_number("10"); assert_eq!(n, 20); } 

Here you should be skeptical about calling unwrap . If the string cannot be parsed as a number, you will get a panic:



 thread '<main>' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }', /home/rustbuild/src/rust-buildbot/slave/beta-dist-rustc-linux/build/src/libcore/result.rs:729 

This is pretty unpleasant, and if something like this happened in the library you use, you might be reasonably angry. So we should try to handle the error in our function, and let the caller decide what to do with it. This means the need to change the type that is returned by double_number . But which one? To understand this, you need to look at the signature of parse from the standard library:



 impl str { fn parse<F: FromStr>(&self) -> Result<F, F::Err>; } 

Hmm At least we know that we must use Result . It is possible that the method could return Option . In the end, the string is either parsed as a number or not, is it? This is, of course, a reasonable way, but the internal implementation knows why the string did not split as an integer. (This may be an empty string, or incorrect numbers, too large or too short a length, etc.) Thus, using Result makes sense, because we want to provide more information than just “absence”. We want to say why the conversion failed. You should think in a similar way when faced with the choice between Option and Result . If you can provide detailed error information, then you probably should. (We'll talk more about this later.)



Good, but how do we write our return type? The parse method is generic for all different types of numbers from the standard library. We could (and probably should) also make our function generalized, but for now let's dwell on a specific implementation. We are only interested in the i32 type, so we should FromStr (do a search in your browser for the string “FromStr”) and look at its associated type Err . We do this to determine the specific type of error. In this case, this is std::num::ParseIntError . Finally, we can rewrite our function:



 use std::num::ParseIntError; fn double_number(number_str: &str) -> Result<i32, ParseIntError> { match number_str.parse::<i32>() { Ok(n) => Ok(2 * n), Err(err) => Err(err), } } fn main() { match double_number("10") { Ok(n) => assert_eq!(n, 20), Err(err) => println!("Error: {:?}", err), } } 

Not bad, but we had to write a lot more code! And we are again annoyed by the variable analysis.



Combinators rush to the rescue! Like Option , Result has many combinators defined as methods. There is a large list of combinators common between Result and Option . And the map is included in this list:



 use std::num::ParseIntError; fn double_number(number_str: &str) -> Result<i32, ParseIntError> { number_str.parse::<i32>().map(|n| 2 * n) } fn main() { match double_number("10") { Ok(n) => assert_eq!(n, 20), Err(err) => println!("Error: {:?}", err), } } 

All expected methods are implemented for Result , including unwrap_or and and_then . In addition, since Result has a second type parameter, there are combinators that only affect the error value, such as map_err (analog map ) and or_else (analog and_then ).



Creating a Result nickname


In the standard library, you can often see types like Result<i32> . But wait, since Result with two type parameters. How can we get around this by pointing out only one of them? The answer lies in the definition of an alias of the Result type, which fixes one of the parameters with a specific type. The type of error is usually fixed. For example, our previous example with converting strings to numbers can be rewritten as:



 use std::num::ParseIntError; use std::result; type Result<T> = result::Result<T, ParseIntError>; fn double_number(number_str: &str) -> Result<i32> { unimplemented!(); } 

Why do we do this? Well, if we have a lot of functions that can return ParseIntError , then it is much more convenient to define an alias that always uses ParseIntError , so we will not repeat all the time.



The most notable use of this approach in the standard library is the alias io::Result . As a rule, it is enough to write io::Result<T> to make it clear that you are using a type alias from the io module, and not the usual definition from std::result . (This approach is also used for fmt::Result )



Short digression: unwrap is not necessarily evil


If you were attentive, you may have noticed that I took a rather tough stance against methods like unwrap that can cause panic and interrupt the execution of your program. Basically , this is good advice.



Nevertheless, unwrap can still be used wisely. The factors that justify the use of unwrap are somewhat vague, and reasonable people may disagree with me. I will summarize my opinion on this issue:




This is probably not an exhaustive list. In addition, when using Option often better to use the expect method. This method does exactly the same as unwrap , except that in case of a panic it will print your message. This will allow you to better understand the cause of the error, because a specific message will be displayed, and not just “called unwrap on a None value”.



My advice comes down to this: use common sense. There are reasons why words like “never do X” or “Y is considered harmful” will not appear in this article. , , , , . , .



, Rust unwrap , .




, Option<T> , Result<T, SomeError> . , Option , Result ? Result<T, Error1> Result<T, Error2> ? — , .



Option Result


, Option , , Result . , , .



, . Option Result . , ?



:



 use std::env; fn main() { let mut argv = env::args(); let arg: String = argv.nth(1).unwrap(); //  1 let n: i32 = arg.parse().unwrap(); //  2 println!("{}", 2 * n); } 

Option Result , , , , .



, argv.nth(1) Option , arg.parse() Result . . Option Result , — Option Result . , ( env::args() ) , . String . Let's try:



 use std::env; fn double_arg(mut argv: env::Args) -> Result<i32, String> { argv.nth(1) .ok_or("Please give at least one argument".to_owned()) .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string())) } fn main() { match double_arg(env::args()) { Ok(n) => println!("{}", n), Err(err) => println!("Error: {}", err), } } 

c . -, Option::ok_or . Option Result . , , Option None . , , :



 fn ok_or<T, E>(option: Option<T>, err: E) -> Result<T, E> { match option { Some(val) => Ok(val), None => Err(err), } } 

, — Result::map_err . , Result::map , , Result . Result k(...) , .



map_err , (- and_then ). Option<String> ( argv.nth(1) ) Result<String, String> , ParseIntError arg.parse() String .




IO — , , Rust. IO .



. , . 2 .



unwrap , unwrap . , , , . , , .



 use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> i32 { let mut file = File::open(file_path).unwrap(); //  1 let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); //  2 let n: i32 = contents.trim().parse().unwrap(); //  3 2 * n } fn main() { let doubled = file_double("foobar"); println!("{}", doubled); } 

(: AsRef , std::fs::File::open . .)



, :



  1. .
  2. .
  3. .

std::io::Error . std::fs::File::open std::io::Read::read_to_string . ( , Result , . Result , , , io::Error .) std::num::ParseIntError . , io::Error . .



file_double . , , , - . , , , . , i32 , . , i32 - .



, : : Option Result ? , , Option . - , None . , , , . , . , Result<i32, E> . E ? , . String . , :



 use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> { File::open(file_path) .map_err(|err| err.to_string()) .and_then(|mut file| { let mut contents = String::new(); file.read_to_string(&mut contents) .map_err(|err| err.to_string()) .map(|_| contents) }) .and_then(|contents| { contents.trim().parse::<i32>() .map_err(|err| err.to_string()) }) .map(|n| 2 * n) } fn main() { match file_double("foobar") { Ok(n) => println!("{}", n), Err(err) => println!(": {}", err), } } 

. , . . file_double Result<i32, String> , . : and_then , map map_err .



and_then , . , : . , and_then .



map , Ok(...) Result . , , map Ok(...) ( i32 ) 2 . , . map .



map_err — , . , , map , , Err(...) Result . — String . io::Error , num::ParseIntError ToString , to_string , .



, - . , . : .



return


. return . return file_double , .



 use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> { let mut file = match File::open(file_path) { Ok(file) => file, Err(err) => return Err(err.to_string()), }; let mut contents = String::new(); if let Err(err) = file.read_to_string(&mut contents) { return Err(err.to_string()); } let n: i32 = match contents.trim().parse() { Ok(n) => n, Err(err) => return Err(err.to_string()), }; Ok(2 * n) } fn main() { match file_double("foobar") { Ok(n) => println!("{}", n), Err(err) => println!(": {}", err), } } 

- , , , , , , . match if let . , ( ).



? , — , , . , . — .



try!


Rust — try! . , , , . , , .



`try!:



 macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(err), }); } 

( . ).



try! . , , :



 use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> { let mut file = try!(File::open(file_path).map_err(|e| e.to_string())); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(|e| e.to_string())); let n = try!(contents.trim().parse::<i32>().map_err(|e| e.to_string())); Ok(2 * n) } fn main() { match file_double("foobar") { Ok(n) => println!("{}", n), Err(err) => println!(": {}", err), } } 

map_err - , try! , String . , , map_err ! , - .




, , String .



String , , , . , String .



, , , . , , . , .



, String . , , , . , String — . , , , . (, , ).



, io::Error io::ErrorKind , , , -. , - . (, BrokenPipe , NotFound .) io::ErrorKind , , String .



, String , , . , .



- enum . , io::Error , num::ParseIntError , :



 use std::io; use std::num; //   `Debug` ,   ,     `Debug`. //           CliError #[derive(Debug)] enum CliError { Io(io::Error), Parse(num::ParseIntError), } 

. , CliError , :



 use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> { let mut file = try!(File::open(file_path).map_err(CliError::Io)); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(CliError::Io)); let n: i32 = try!(contents.trim().parse().map_err(CliError::Parse)); Ok(2 * n) } fn main() { match file_double("foobar") { Ok(n) => println!("{}", n), Err(err) => println!(": {:?}", err), } } 

— map_err(|e| e.to_string()) ( ) map_err(CliError::Io) map_err(CliError::Parse) . . , String , enum , CliError , , , , .



, , String , . , . , .



,


, std::error::Error std::convert::From . Error , From .



Error


Error :



 use std::fmt::{Debug, Display}; trait Error: Debug + Display { /// A short description of the error. fn description(&self) -> &str; /// The lower level cause of this error, if any. fn cause(&self) -> Option<&Error> { None } } 

, , , . , . , , :




, Error Debug Display . , Error . rror , , - (trait object). Box<Error> , &Error . , cause &Error , -. Error -.



, Error . , :



 use std::io; use std::num; //   `Debug` ,   ,     `Debug`. //           CliError #[derive(Debug)] enum CliError { Io(io::Error), Parse(num::ParseIntError), } 

: I . , , enum .



Error :



 use std::error; use std::fmt; impl fmt::Display for CliError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { //       `Display`, //        CliError::Io(ref err) => write!(f, "IO error: {}", err), CliError::Parse(ref err) => write!(f, "Parse error: {}", err), } } } impl error::Error for CliError { fn description(&self) -> &str { //       `Error`, //        match *self { CliError::Io(ref err) => err.description(), CliError::Parse(ref err) => err.description(), } } fn cause(&self) -> Option<&error::Error> { match *self { //        `err` //    (`&io::Error`  `&num::ParseIntError`) //  - `&Error`.        `Error`. CliError::Io(ref err) => Some(err), CliError::Parse(ref err) => Some(err), } } } 

, Error : description cause .



From


std::convert::From :



 trait From<T> { fn from(T) -> Self; } 

, ? From , - ( , « » , , Self ). From — , .



, From :



 let string: String = From::from("foo"); let bytes: Vec<u8> = From::from("foo"); let cow: ::std::borrow::Cow<str> = From::from("foo"); 

, From . ? , :



 impl<'a, E: Error + 'a> From<E> for Box<Error + 'a> 

, , Error , - Box<Error> . , .



, , , io::Error and num::ParseIntError ? Error , From :



 use std::error::Error; use std::fs; use std::io; use std::num; //    let io_err: io::Error = io::Error::last_os_error(); let parse_err: num::ParseIntError = "not a number".parse::<i32>().unwrap_err(); // ,  let err1: Box<Error> = From::from(io_err); let err2: Box<Error> = From::from(parse_err); 

. err1 err2 — -. , , err1 err2 . , err1 err2 , — From::from . , From::from .



, , , .



— try! .



try!


try! :



 macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(err), }); } 

. :



 macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(::std::convert::From::from(err)), }); } 

, : From::from . try! , .



try! , , , :



 use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> { let mut file = try!(File::open(file_path).map_err(|e| e.to_string())); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(|e| e.to_string())); let n = try!(contents.trim().parse::<i32>().map_err(|e| e.to_string())); Ok(2 * n) } 

, map_err . , — , From . , From , Box<Error> :



 use std::error::Error; use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, Box<Error>> { let mut file = try!(File::open(file_path)); let mut contents = String::new(); try!(file.read_to_string(&mut contents)); let n = try!(contents.trim().parse::<i32>()); Ok(2 * n) } 

. - , try! :



  1. .
  2. .
  3. .

, , , unwrap .



: Box<Error> . Box<Error> , () . , , , String , , description cause , : Box<Error> . (: , Rust , , ).



CliError .




try! , From::from . Box<Error> , , .



, , : . , :



 use std::fs::File; use std::io::{self, Read}; use std::num; use std::path::Path; //   `Debug` ,   ,     `Debug`. //           CliError #[derive(Debug)] enum CliError { Io(io::Error), Parse(num::ParseIntError), } fn file_double_verbose<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> { let mut file = try!(File::open(file_path).map_err(CliError::Io)); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(CliError::Io)); let n: i32 = try!(contents.trim().parse().map_err(CliError::Parse)); Ok(2 * n) } 

, map_err . Why? try! From . , From , io::Error num::ParseIntError CliError . ! CliError , From :



 use std::io; use std::num; impl From<io::Error> for CliError { fn from(err: io::Error) -> CliError { CliError::Io(err) } } impl From<num::ParseIntError> for CliError { fn from(err: num::ParseIntError) -> CliError { CliError::Parse(err) } } 

From CliError . . , .



, file_double :



 use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> { let mut file = try!(File::open(file_path)); let mut contents = String::new(); try!(file.read_to_string(&mut contents)); let n: i32 = try!(contents.trim().parse()); Ok(2 * n) } 

, — map_err . , try! From::from . , From , .



file_double , - , , , :



 use std::io; use std::num; enum CliError { Io(io::Error), ParseInt(num::ParseIntError), ParseFloat(num::ParseFloatError), } 

From :



 use std::num; impl From<num::ParseFloatError> for CliError { fn from(err: num::ParseFloatError) -> CliError { CliError::ParseFloat(err) } } 

That's all!




, . ( ErrorKind ), ( ParseIntError ). , , . , , .



, Error . . Error , ( fmt::Debug fmt::Display ).



, From . ( ) . , csv::Error From io::Error byteorder::Error .



, , Result , , . io::Result fmt::Result .



Conclusion


, Rust. . . , .




«The Rust Programming Language». . , , Rust, Rust .

Source: https://habr.com/ru/post/270371/


All Articles