Rust -- How to handle error precisely and gracefully? - error-handling

Sorry for naming this a so ambiguous title, but I can't come up with a better one.
The read_dir() method defined in std::fs returns instances of io::Result<DirEntry>, which is the alias of Result<DirEntry, io::Error>. When the caller does not exist on file system, it returns the error.
Now my code is
dir_a.read_dir();
dir_b.read_dir();
dir_c.read_dir();
And dir_a, dir_b, dir_c all may not exist. So these three statements may return the same io::Error, but for my program, dir_a, dir_b and dir_c have different meaning, where I want to handle the error for each one respectively.
So I defined my own enum MyError as
enum MyError {
Dir_a_not_exist,
Dir_b_not_exist,
Dir_c_not_exist,
}
How can I transform the same io::Error to my different three MyError?
My ugly way is to
match dir_a.read_dir() {
Ok => match dir_b.read_dir() {
Ok => match dir_c.read_dir() {
Ok => { /* do something */ },
Err => return MyError::Dir_c_not_exist,
},
Err => return MyError::Dir_b_not_exist,
},
Err => return MyError::Dir_a_not_exist,
};
Is there any graceful way I can handle this?

Result has a function called or, that allows you to forward the result if it's Ok, or transform it if it's an error. With that, you can do something like this:
fn foo(dir_a: &Path, dir_b: &Path, dir_c: &Path) -> Result<(), MyError> {
dir_a.read_dir().or(Err(MyError::DirAnotExist))?;
dir_b.read_dir().or(Err(MyError::DirBnotExist))?;
dir_c.read_dir().or(Err(MyError::DirCnotExist))?;
/* do something */
Ok(())
}

Related

error: expected type 'type' -- while trying to return error from an error set

I'm new to Zig and am trying to learn how error-handling and error sets work.
If I run
const erro = error{Oops};
fn failingFunction() erro.Oops!void {
return erro.Oops;
}
test "returning an error" {
failingFunction() catch |err| {
try expect(err == erro.Oops);
return;
};
}
I get an error:
error: expected type 'type', found 'erro'
fn failingFunction() erro.Oops!void {
^
./test.zig:45:31: note: referenced here
fn failingFunction() erro.Oops!void {
^
./test.zig:50:5: note: referenced here
failingFunction() catch |err| {
But when i use erro!void instead of erro.Oops!void as the funtion return type, the tests pass. Why is this so?
Please help. How do error unions work in the language? Thank You.
EDIT: The above is a modified function. The original function is
fn failingFunction() error{Oops}!void {
return error.Oops;
}
from this article: https://ziglearn.org/chapter-1/ in the "Errors" section. I wanted to experiment and so out of curiosity I did the above.
in failingFunction exception you have passed an error option (Oops) not error type. that's useless and wrong. overwrite your function with:
fn failingFunction() erro!void {
return erro.Oops;
}

Conditionally Acting on Specific Error Using and_then in Rust

How do I know what error happened when piping Results via and_then in Rust? For example using nested matches I could do this:
use std::num::ParseIntError;
fn get_num(n: u8) -> Result<u8, ParseIntError> {
// Force a ParseIntError
"foo ".parse()
}
fn add_one(n: u8) -> Result<u8, ParseIntError> {
Ok(n + 1)
}
fn main() {
match get_num(1) {
Ok(n) => {
match add_one(n) {
Ok(n) => println!("adding one gives us: {}", n),
Err(why) => println!("Error adding: {}", why)
}
},
Err(why) => println!("Error getting number: {}", why),
}
}
Or I can use and_then to pipe the result of the first function to the second and avoid a nested match:
match get_num(1).and_then(add_one) {
Ok(n) => println!("Adding one gives us: {}", n),
Err(why) => println!("Some error: {}.", why)
}
How do I conditionally and idiomatically determine the error in the second form, using and_then? Do I have to type check the error? Sure I can display the error like I did above, but let's say I want to preface it with something related to the kind of error it is?
The normal way would be to use map_err to convert the initial errors to something richer you control e.g.
enum MyError {
Parsing,
Adding
}
get_num(1).map_err(|_| MyError::Parsing)
.and_then(|v| add_one(v).map_err(|_| MyError::Adding)
sadly as you can see this requires using a lambda for the and_then callback: and_then must return the same error type as the input, so here it has to yield a Result<_, MyError>, wjhich is not the return type of add_one.
And while in other situations we could recover somewhat with ?, here the same original error type is split into two different values.

Cannot move io::Error out of Peekable Result

I'm just trying to propagate the IO error:
enum MyError {
EOF,
IO(std::io::Error),
}
fn peek_byte<R>(mut p: Peekable<Bytes<R>>) -> Result<u8, MyError>
where
R: Read,
{
match p.peek() {
None => Err(MyError::EOF),
Some(Err(e)) => Err(MyError::IO(*e)), // <==== error is here
Some(Ok(byte)) => Ok(*byte),
}
}
But, I get the following error:
error[E0507]: cannot move out of `*e` which is behind a shared reference
--> src/main.rs:17:41
|
17 | Some(Err(e)) => Err(MyError::IO(*e)),
| ^^ move occurs because `*e` has type `std::io::Error`, which does not implement the `Copy` trait
I actually understand all of this. I know why I'm getting the error, and what the error means. What I don't know is how to accomplish my task and propagate the IO error into my error type.
I have tried e, *e, e.clone(), *e.clone(), *(e.clone()), but they all either produce a "type mismatch" or a "cannot move" error.
The Peekable iterator holds ownership of the next value of its internal iterator and returns references via peek, but if you actually want the owned value you just call next as usual (which does advance the iterator but I think that's okay in this case since you're not actually consuming any content from the iterator but just trying to return an error):
use std::io;
use std::io::Bytes;
use std::io::Read;
use std::iter::Peekable;
enum MyError {
EOF,
IO(io::Error),
}
fn peek_byte<R>(mut p: Peekable<Bytes<R>>) -> Result<u8, MyError>
where
R: Read,
{
match p.peek() {
None => Err(MyError::EOF),
Some(Err(e)) => Err(MyError::IO(p.next().unwrap().unwrap_err())),
Some(Ok(byte)) => Ok(*byte),
}
}
playground

Why are the strings in my iterator being concatenated?

My original goal is to fetch a list of words, one on each line, and to put them in a HashSet, while discarding comment lines and raising I/O errors properly. Given the file "stopwords.txt":
a
# this is actually a comment
of
the
this
I managed to make the code compile like this:
fn stopword_set() -> io::Result<HashSet<String>> {
let words = Result::from_iter(
BufReader::new(File::open("stopwords.txt")?)
.lines()
.filter(|r| match r {
&Ok(ref l) => !l.starts_with('#'),
_ => true
}));
Ok(HashSet::from_iter(words))
}
fn main() {
let set = stopword_set().unwrap();
println!("{:?}", set);
assert_eq!(set.len(), 4);
}
Here's a playground that also creates the file above.
I would expect to have a set of 4 strings at the end of the program. To my surprise, the function actually returns a set containing a single string with all words concatenated:
{"aofthethis"}
thread 'main' panicked at 'assertion failed: `(left == right)` (left: `1`, right: `4`)'
Led by a piece of advice in the docs for FromIterator, I got rid of all calls to from_iter and used collect instead (Playground), which has indeed solved the problem.
fn stopword_set() -> io::Result<HashSet<String>> {
BufReader::new(File::open("stopwords.txt")?)
.lines()
.filter(|r| match r {
&Ok(ref l) => !l.starts_with('#'),
_ => true
}).collect()
}
Why are the previous calls to from_iter leading to unexpected inferences, while collect() works just as intended?
A simpler reproduction:
use std::collections::HashSet;
use std::iter::FromIterator;
fn stopword_set() -> Result<HashSet<String>, u8> {
let input: Vec<Result<_, u8>> = vec![Ok("foo".to_string()), Ok("bar".to_string())];
let words = Result::from_iter(input.into_iter());
Ok(HashSet::from_iter(words))
}
fn main() {
let set = stopword_set().unwrap();
println!("{:?}", set);
assert_eq!(set.len(), 2);
}
The problem is that here, we are collecting from the iterator twice. The type of words is Result<_, u8>. However, Result also implements Iterator itself, so when we call from_iter on that at the end, the compiler sees that the Ok type must be String due to the method signature. Working backwards, you can construct a String from an iterator of Strings, so that's what the compiler picks.
Removing the second from_iter would solve it:
fn stopword_set() -> Result<HashSet<String>, u8> {
let input: Vec<Result<_, u8>> = vec![Ok("foo".to_string()), Ok("bar".to_string())];
Result::from_iter(input.into_iter())
}
Or for your original:
fn stopword_set() -> io::Result<HashSet<String>> {
Result::from_iter(
BufReader::new(File::open("stopwords.txt")?)
.lines()
.filter(|r| match r {
&Ok(ref l) => !l.starts_with('#'),
_ => true
}))
}
Of course, I'd normally recommend using collect instead, as I prefer the chaining:
fn stopword_set() -> io::Result<HashSet<String>> {
BufReader::new(File::open("stopwords.txt")?)
.lines()
.filter(|r| match r {
&Ok(ref l) => !l.starts_with('#'),
_ => true,
})
.collect()
}

Simplification possible in example using print! and flush?

I started programming Rust a couple of days ago by working through the official documentation. Now I'm trying to challenge my understanding of Rust by working through the book "Exercises for Programmers" by Brian P. Hogan (The Pragmatic Programmers).
The first exercise is to write a program that asks the user for a name and prints out a greeting using that name. Input, string concatenation and output should be done in three distinct steps.
What is your name? Patrick
Hello, Patrick, nice to meet you.
The name will be entered at the same line as the initial prompt. Here's my solution:
use std::io;
use std::io::Write;
fn main() {
print!("What is your name? ");
match io::stdout().flush() {
Ok(_) => print!(""),
Err(error) => println!("{}", error),
}
let mut name = String::new();
match io::stdin().read_line(&mut name) {
Ok(_) => {
name = name.trim().to_string();
if name.len() > 0 {
let greeting = "Hello, ".to_string() + &name + &", nice to meet you!".to_string();
println!("{}", greeting);
} else {
println!("No name entered, goodbye.");
}
}
Err(error) => println!("{}", error),
}
}
The print! macro doesn't actually output the prompt until I call flush. flush needs error handling, so I need both to handle the Ok and the Err case. In case of Ok, there's nothing useful to do, so I just print! an empty string.
Is there a shorter way to handle this? Maybe the error handling can be skipped or simplified somehow, or the whole print!/flush approach is the wrong one. (Everything works fine, but I could write this shorter in C, after all...)
As other people have said, make sure to read the error handling chapter.
In most cases, you don't want to use println! to report errors. Either you should return the error from your function and let the caller deal with it, or you should use panic! to abort that thread and potentially the process.
match io::stdout().flush() {
Ok(_) => print!(""),
Err(error) => println!("{}", error),
}
Instead of printing nothing (which is inefficient), you can just... do nothing:
match io::stdout().flush() {
Ok(_) => (),
Err(error) => println!("{}", error),
}
Since you don't care about the success case, you can use an if let:
if let Err(error) = io::stdout().flush() {
println!("{}", error);
}
Replacing the println with a panic! would be even better:
if let Err(error) = io::stdout().flush() {
panic!("{}", error);
}
This is almost exactly what Option::unwrap does (source), except it also returns the successful value when present:
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
However, it's even better to use Option::expect which allows you to specify an additional error message:
io::stdout().flush().expect("Unable to flush stdout");
Applying that twice:
use std::io::{self, Write};
fn main() {
print!("What is your name? ");
io::stdout().flush().expect("Unable to flush stdout");
let mut name = String::new();
io::stdin()
.read_line(&mut name)
.expect("Unable to read the line");
let name = name.trim();
if !name.is_empty() {
println!("Hello, {}, nice to meet you!", name);
} else {
println!("No name entered, goodbye.");
}
}
Note that there's no need to re-allocate a String, you can just shadow name, and there's no need to use format just to print out stuff.
Since Rust 1.26.0, you could also choose to return a Result from main:
use std::io::{self, Write};
fn main() -> Result<(), io::Error> {
print!("What is your name? ");
io::stdout().flush()?;
let mut name = String::new();
io::stdin().read_line(&mut name)?;
let name = name.trim();
if !name.is_empty() {
println!("Hello, {}, nice to meet you!", name);
} else {
println!("No name entered, goodbye.");
}
Ok(())
}
but I could write this shorter in C, after all...
I would encourage / challenge you to attempt this. Note that every memory allocation in this program is checked, as is every failure case dealing with the standard output. Many people are not aware that C's printf returns an error code that you should be checking. Try outputting to a pipe that has been closed for an example.