2

i want to read a textfile and convert all lines into int values. I use this code. But what i really miss here is a "good" way of error handling.

use std::{
    fs::File,
    io::{prelude::*, BufReader},
    path::Path
};

fn lines_from_file(filename: impl AsRef<Path>) -> Vec<i32> {
    let file = File::open(filename).expect("no such file");
    let buf = BufReader::new(file);
    buf.lines()
        .map(|l| l.expect("Could not parse line"))
        .map(|l:String| l.parse::<i32>().expect("could not parse int"))
        .collect()
}

Question: How to do proper error handling ? Is this above example "good rust code" ? or should i use something like this :

fn lines_from_file(filename: impl AsRef<Path>) -> Vec<i32> {
    let file = File::open(filename).expect("no such file");
    let buf = BufReader::new(file);
    buf.lines()
        .map(|l| l.expect("Could not parse line"))
        .map(|l:String| match l.parse::<i32>() {
            Ok(num) => num,
            Err(e) => -1 //Do something here 
        }).collect()
}
Skary
  • 387
  • 1
  • 4
  • 18
  • 1
    As listed in the Rust Lang book, use expect when the error is generalized, when there could be multiple reasons why this error ocurred, you could match against it and print the error on the stderr, or just panic if this is designed for programmers – Rafaelplayerxd YT Dec 02 '21 at 21:47
  • => https://play.integer32.com/?version=stable&mode=debug&edition=2021&gist=eaa9b92bbebf756893ed05c63d10b25c – Stargateur Dec 02 '21 at 21:49
  • 1
    Your first example is good if your program *needs* valid input, and should not be able to continue if there is a problem with the data. The second one is more tolerant, and will allow bad values which you can work around by ignoring -1 or something. Either way can be good, but you should try to be consistent. (since you panic on a bad line, I would suggest you panic on a bad parse as well, but I don't know your end goal) – Jeremy Meadows Dec 02 '21 at 22:15
  • "Good" error handling depends entirely on _what you want to do when an error occurs_. `expect()` is fine in some cases. – Chayim Friedman Dec 03 '21 at 10:41

1 Answers1

12

You can actually collect into a Result<T, E>. See docs

So you could collect into a Result<Vec<i32>, MyCustomErrorType>. This works when you transform your iterator in an iterator which returns a Result<i32, MyCustomErrorType>. The iteration stops at the first Err you map.

Here's your working code example. I used the thiserror crate for error handling

use std::{
    fs::File,
    io::{prelude::*, BufReader},
    num::ParseIntError,
    path::Path,
};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum LineParseError {
    #[error("Failed to read line")]
    IoError(#[from] std::io::Error),
    #[error("Failed to parse int")]
    FailedToParseInt(#[from] ParseIntError),
}

fn lines_from_file(filename: impl AsRef<Path>) -> Result<Vec<i32>, LineParseError> {
    let file = File::open(filename).expect("no such file");
    let buf = BufReader::new(file);
    buf.lines().map(|l| Ok(l?.parse()?)).collect()
}

Some small explanation of how the code works by breaking down this line of code:

buf.lines().map(|l| Ok(l?.parse()?)).collect()
  • Rust infers that we need to collect to a Result<Vec<i32>, LineParseError> because the return type of the function is Result<Vec<i32>, LineParseError>
  • In the mapping method we write l? this makes the map method return an Err if the l result contains an Err, the #[from] attribute on LineParseError::IoError takes care of the conversion
  • The .parse()? works the same way: #[from] on LineParseError::FailedToParseInt takes care of the conversion
  • Last but not least our method must return Ok(...) when the mapping does succeed, this makes the collect into a Result<Vec<i32>, LineParseError> possible.
Jeroen Vervaeke
  • 1,040
  • 9
  • 20