Confusion about unwrap_or_else

Hello,
I am hoping someone can help me clear up some confusion about error handling. I am trying to understand how to use "unwrap_or_else". In the code below, the program attempts to create a file in a path that does not exist.
If the path did exist and the file was successfully created, I understand that a Result<std::fs::File> will be returned which will be unwrapped and assigned to "_file". However, it is the "or_else" component I am interested in. I would like to have the program simply print out an error message and continue running.

If I remove the line process::exit(1); below, the compiler complains with "expected struct std::fs::File, found ()". Why can't I just take the Error returned from the the File::create function, print out the details and then continue?

How can I modify the code to do this?

I have read through the documentation and see that I can update the main function to have a return type of std::io::Result<()> and then use let mut _file = File::create("/home/user/doesnotexist/text.txt")?; but I don't want to do this as it terminates the program when the error is propagated back to main.

Sorry if I have overlooked something simple. I just want to handle the erorr with a simple message and then continue running without having to write ugly match statements everywhere. The "unwrap_or_else" function seems perfect but the else part seems to insist on having a panic or termination.

Thanks!

use std::fs::File;
use std::process;

    fn main() {
        let mut _file = File::create("/home/user/doesnotexist/text.txt").unwrap_or_else(|e| {
            println!("Could not create file: {}", e);
            process::exit(1);
        });
        
        println!("Continuing with program.....")
    }

The intended use of unwrap_or_else is to give a default return value in case of an error. This is why you have to return a File from the closure. This isn't really useful in your case - the only file you care about is the one you're creating, and there's not really a meaningful default value to give if it fails to save.

Looking at your code, you don't actually care about getting a return value from File::create - you just want to check if there was an error, and run some logic if so. if let is your friend here :slight_smile:

if let Err(e) = File::create("/home/user/doesnotexist/text.txt") {
    println!("Could not create file: {}", e);
}

// ...and onwards

If this is a reduced-down version of your code and you actually do need access to the file, you'd need to either use a match to branch your code depending on whether the file is present or not, or do an early return from the function when there's an error.

2 Likes

Thanks for your reply 17cupsofcoffee.

I read from the documentation that a "unwrap_or_default" method exists and that perhaps this is more suitable for defining a default value?

I must have misunderstood but I thought that with "unwrap_or_else", I could define exactly what to do in the "else" part so I am not sure why I still have to specify a default value for a std::fs:File.

I don't really understand why adding a panic or termination (instead of a std::fs::File value) in the else component results in the code compiling ok.

Thank you for your suggestion about the if let statement though. This is certainly useful :slight_smile:

Regards,
djme.

There's three variants of the unwrap_or methods, all of which serve the purpose of giving a default value in the case of an error:

  • unwrap_or, which just takes a default value directly. This is good for lightweight types (like an i32), but bad if the default value is expensive to create, as the code in the argument will always get run.
  • unwrap_or_else, which takes a closure that produces a default value. This allows you to defer creation of the default value until the error occurs, which is good if that value is expensive to create.
  • unwrap_or_default is basically the same as unwrap_or_else, but instead of using a closure, it uses the type's implementation of the Default trait to product a default value.

The reason that the code started working when you added a panic/termination is because those methods/macros never return. The type system represents this as a special return type, ! (referred to as the 'never' type).

Because an expression of type ! will never produce a value, it can coerce into any type, effectively letting you use it in place of any other value. For example, that's why let x: File = panic!() is a valid statement.

Hopefully that info is helpful/interesting :slight_smile:

3 Likes

That is because Rust is an expression language, not a statement language. Almost every construct results in a value, which necessarily has a type. Since the first part of your expression established the type as std::fs:File, the "or_else" alternative was constrained to produce the same type, or as @17cupsofcoffee pointed out, a value (e.g., !) that could be coerced to that type.

1 Like

Do error handling once:

fn main() {
   if let Err(e) = run() {
      eprintln!("error: {}", e);
      process::exit(1);
   }
}

and then use the ? operator:

fn run() -> io::Result<()> {
    let mut file = File::create("/home/user/doesnotexist/text.txt")?;
}

(it's also possible to use ? directly in main, but this way you get clean error output instead of unwrap-barf)

3 Likes

Thanks 17cupsofcoffee & TomP for clarifying about the type coercion.

Also, thanks kornel for providing the suggested code structure. I did see a similar style outlined in the Rust Book when the reader was walked through the "minigrep" example. I think this structure looks the tidiest/cleanest to me and uses the "if let" statement that 17cupsofcoffee suggested earlier.

Thanks everyone for your informative replies.
djme.

One more :stuck_out_tongue: If you just want to get something up and running quick, you can actually use Result and ? in main on newer versions of Rust:

fn main() -> io::Result<()> {
    let mut file = File::create("/home/user/doesnotexist/text.txt")?;
    Ok(())
}

This will print the Debug implementation of the error and exit with an error code if something fails. The main downside of it is that you don't have control over the error handling/output - if that's something you want/need, that's when you'd want to use Kornel's solution.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.