Is There a Way to Escape a Closure to Throw an Error?

So this is kind of continuing from a post I made a while back that I found a decent solution for at the time, but I ran into it again:

So here is what I'm working with:

let build_dir_canonical = build_dir.canonicalize()?;
for entry in WalkDir::new(charm_path).into_iter().filter_entry(|e| {
        // Don't include any files in the build dir
        let entry_path = if let Ok(path) = e.path().canonicalize() {
            path
        } else {
            return false;
        };
        !entry_path.strip_prefix(&build_dir_canonical).is_ok()
    }) {
        // Do stuff...
    }

The goal is to walk through everything in the charm_dir and ignore every path in there that is inside of the build_dir.

To be clear, right now it is working, but the problem is that if the e.path().canonicalize() function fails for some reason, the path will be filtered out when I instead should probably be exiting the program with an error message because I could not canonicalize the path.

It seems like there is no way, short of panicking and putting the block in a catch_unwrap(), to get an error to propagate out of the closure and into the function that is passing in the closure.

Does anybody have any pointers on this. In reality it is probably fine just to skip over paths that I can't canonicalize for some reason, but at the same time I feel like that is silently ignoring an error that could potentially come back to bite the user somehow.

This seems like an API issue: the library should be offering a

fn try_filter_entry<E> (
    self: ::walkdir::IntoIter,
    predicate: Predicate,
) -> impl Iterator<Item = Result<Self::Item, E>>
where
    Predicate : FnMut(&DirEntry) -> Result<bool, E>,

or something along those lines, so that treating an item can error which leads to your iterating over Result<> rather than the usual entries (which they so happen to be Results on their own):

for result_entry in
    ::walkdir::WalkDir::new(charm_path)
        .into_iter()
        .try_filter_entry(|e| Ok({
            let path = e.path.canonicalize()?;
            path.strip_prefix(&build_dir_canonical).is_err()
        }))
{
    let entry = result_entry?; // Error can now bubble up.
    // Do stuff...
}

And to get the API enhancement you could submit an issue (or even better, a PR) suggesting it.


Now, if you do not feel like diving into ::walkdir internals to add a .try_filter_entry adaptor, you must "break" purity and use one of the following "hacks" (which, given how expensive recursively exploring a directory tree may be, have negligible overhead):

  • use unwinding:

    1. On the closure:

      let entry_path = match e.path().canonicalize() {
          | Ok(it) => it,
          | Err(err) => panic!(err), // no formatting on purpose
      };
      
    2. Outside the for loop:

      use ::std::panic;
      panic::catch_unwind(|| {
          for entry in ... { ... }
      }).map_err(|box_any| {
          match box_any.downcast::<::walkdir::Error>() {
              | Ok(boxed_error) => *boxed_error,
              | Err(box_any) => panic::resume_unwind(box_any),
          }
      })? // Error bubbles up
      
      
  • Track state with a custom Option<Error> variable:

    let mut errored = None::<::walkdir::Error>;
    for entry in
        ::walkdir::WalkDir::new(charm_path)
            .into_iter()
            .filter_entry(|e| {
                if errored.is_some() { return false; }
                let path = match e.path.canonicalize() {
                    | Ok(it) => it,
                    | Err(err) => {
                        errored = Some(err);
                        return false;
                    },
                };
                path.strip_prefix(&build_dir_canonical)
                    .is_err()
            })
    {
        // Error can now bubble up.
        if let Some(err) = errored { return Err(err); }
        // Do stuff...
    }    
    
1 Like

Thank you, that makes sense. :+1:

I'll look into making a PR for a try_filter_entry function for walkdir then. :slight_smile:

1 Like