How to handle Result only after an amount of tries?

I am building a simple tool that fetches data from an API. So I have this function:

fn fetch_some_data(args: Args) -> anyhow::Result<String> {
    // call the API, return JSON
}

I am using anyhow, by the way.

If it fails, I want to call it again after 15 seconds, repeat 4 times, and only if by then I still have an error, return the error. Something like:

fn try_four_times(args) -> anyhow::Result<String>{
    for _try in 1..4 {
        match fetch_some_data(args) {
            Ok(json) => return Ok(json),
            Err(some_error) => {
                 // retry, but how?
            },
        }
    thread::sleep(time::Duration::from_secs(15);
    }
    // after having tried four times
    Err(the_error_that_previously_appeared)
}

I don't even know how to ask Duckduckgo, what are the keywords I am missing here?

It sounds like you are looking for the continue keyword.

6 Likes

It looks like your goal is to get some_error out from the final of four match arms in the for loop. You could put an if _try >= 4 { return Err(some_error) } else { continue } in place of the // retry, but how? line. Then that will pass until it hits the return limit.

Side note: I also think you might be looking for 1..=4 or 0..4 on the range.

Okay I have written this, thanks to both of your replies:

    pub fn try_four_times(&mut self) -> anyhow::Result<()> {
        for _try in 1..=4 {
            match self.request_for_json() {
                Ok(json_body) => {
                    // do stuff
                    return Ok(());
                }
                Err(some_error) => {
                    if _try < 4 {
                        println!("retrying");
                        continue;
                    } else {
                        return Err(some_error);
                    }
                }
            }
        }
        Ok(())
    }

It works :slight_smile: but I still find this code a bit ugly... I'm sure there must be a more elegant way to do this.

You can also go for this:

pub fn try_four_times(&mut self) -> anyhow::Result<()> {
    let mut num_failures = 0;
    loop {
        match self.request_for_json() {
            Ok(json_body) => {
                // do stuff
                return Ok(());
            }
            Err(some_error) => {
                num_failures += 1;
                if num_failures == 4 {
                    return Err(some_error);
                }
            }
        }
    }
}
3 Likes

That looks better already, thanks a lot to you all !

My version:

fn try_four_times(args:Args) -> anyhow::Result<String>{
    let mut err = None;
    for _ in 0..4 {
        match fetch_some_data(args) {
            Ok(json) => { return Ok(json); }
            Err(e) => { err = Some(e); }
        }
        thread::sleep(time::Duration::from_secs(15));
    }
    // after having tried four times
    Err(err.unwrap())
}

(Playground)


Edit: Another, completely different, approach is to use iterator adapters.

fn try_four_times(args: Args) -> anyhow::Result<String> {
    std::iter::from_fn(|| Some(fetch_some_data(args)))
        .inspect(|r| if r.is_err() {
            thread::sleep(time::Duration::from_secs(15));
        })
        .take(3)
        .find(Result::is_ok)
        .unwrap_or_else(|| fetch_some_data(args))
}

(Playground)

3 Likes

how about

fn try_four_times(args: Args) -> anyhow::Result<String>{
    let mut result;
    for _ in 1..4 {
        result = fetch_some_data(args);
        if result.is_ok(){
            break;
        }
        thread::sleep(time::Duration::from_secs(15);
    }
    // after having tried four times
    result
}
3 Likes

This won't work, the compiler can't infer that the loop body will be executed at least once, and if it doesn't then result would be unitialized.

2 Likes

My boss told me to do make the function recursive, I'm trying things out and I'll keep you updated.

This is crazy!

Another iterative solution:

fn try_n_times(args: Args, n: usize) -> anyhow::Result<String> {
    for _ in 0..n - 1 {
        let result = fetch_some_data(args);
        if result.is_ok() {
            return result;
        }
    }
    
    fetch_some_data(args)
}

The recursive one does indeed read somewhat more naturally:

fn try_n_times(args: Args, n: usize) -> anyhow::Result<String> {
    let result = fetch_some_data(args);
    
    if n <= 1 || result.is_ok() {
        result
    } else {
        try_n_times(args, n - 1)
    }
}
8 Likes

@SkiFire13 you are right should have tested it whoops. Well one could make it work with loop and a counter but thats not as nice anymore.

Is there an "Obfuscated Rust" site anywhere?

If not, do you mind if I put one up and kick it off with that example?

:slight_smile:

Also it rather goes against the DRY principle, what with making the call to `fetch_some_data()' in two places.

3 Likes

Be my guest.

I can fix that:

fn try_four_times(args: Args) -> anyhow::Result<String> {
    // Make this as complicated as you like
    let try_get_data = move || fetch_some_data(args);

    std::iter::from_fn(|| Some(try_get_data()))
        .inspect(|r| if r.is_err() {
            thread::sleep(time::Duration::from_secs(15));
        })
        .take(3)
        .find(Result::is_ok)
        .unwrap_or_else(try_get_data)
}
2 Likes

So now DRY means you are not supposed to call functions more than once? I don't think so. Functions are meant for abstracting away repetitive pieces of code (among others), exactly so that you can call them as many times as you please, without needing to copy-paste.

I don't see how that's obfuscated in any way – it's a perfectly idiomatic iterator chain. Please let's not start the pointless "iterators are hard to read" debate again, it's not productive.

8 Likes

Thank you so much for you answers. I've been having fun with my mentor's idea of a recursive function and I've come up with … wait I wanted your help, the compiler told me to remove a semicolon and now… I've got different, more interesting errors.

debugging

Cheers for the solution with iterators. BTW we really need this obfuscated rust blog!

I'll keep you up with what I settled for.

2 Likes

Here we are:

impl MyStruct {
    fn request_for_json(
        &self,
        retries: u8,
    ) -> anyhow::Result<String> {

        let request = Request::builder()
            .uri("obfuscated-rust-blog-to-come.com")
            .body(())?;

        match request.send() {
            Ok(mut response) => {
                println!("{:#?}", &response);
                let json_body = response.text()?;
                anyhow::Result::Ok(json_body)
            }
            Err(error) => {
                if retries > 0 {
                    println!("let's retry, {} tries left", retries);
                    thread::sleep(time::Duration::from_secs(15));
                    self.request_for_json(retries - 1)
                } else {
                    return Err(anyhow::Error::new(error));
                }
            }
        }
    }
}

It compiles, it even works. What do you think?

I like it.

If it were me I would separate out the mechanics of making a single request from the recursive mechanics of making retries with timeouts. Have a function to make the single request, and cal it from the recursive retry function. As raggy indicates with the recursive example above.

1 Like

I think it's the perfect place for match guards

impl MyStruct {
    fn request_for_json(&self, retries: u8) -> anyhow::Result<String> {
        let request = Request::builder()
            .uri("obfuscated-rust-blog-to-come.com")
            .body(())?;

        match request.send() {
            Ok(mut response) => {
                println!("{:#?}", &response);
                let json_body = response.text()?;
                anyhow::Result::Ok(json_body)
            }
            Err(_) if retries > 0 => {
                println!("let's retry, {} tries left", retries);
                thread::sleep(time::Duration::from_secs(15));
                self.request_for_json(retries - 1)
            }
            Err(error) => Err(anyhow::Error::new(error)),
        }
    }
}

I TRIED match guards and the compiler told me Err(_) is not present. Why ?