Error handling with nested function calls

I am looking into best practices for error handling when it comes to functions that could return an error (could be handled with unwrap(), but instead we want to use error handling). Say I have a rust lib that has a function that simply gets an environment variable and returns it. The env::var crate returns Result<String, VarError> . Option 1 catches the Resut or Error type in a match statement in this lib function and returns either the String read out or a blank String if there was an error (along with a print statement)

Option 1

lib.rs

pub fn fetch_env_var1() -> String {
    // Instead of using unwrap(), use a match to catch the error at the lowest level
    match env::var("ENV_VAR1") {
        Ok(data) => { 
             println!("The env var1 value is: {:?}", data);
             data 
        }
        Err(e) => { 
           println!("Error retrieving env var1: {:?}", e);
           String::new() // Return blank String
     }
}

Now say this function is called by a nested function call in my main loop:

main.rs

fn example_function() -> String {
      fetch_env_var1()
}

fn main() {
    println!("The env var1 value is: {:?}", example_function());
}

OR is it better practice to always propagate up the errors to the highest level? i.e. this below instead:

Option 2

lib.rs

pub fn fetch_env_var1() -> Result<String, Box<dyn std::error::Error>> {
      env::var("ENV_VAR1")?
}
main.rs

fn example_function() -> Result<String, Box<dyn std::error::Error>> {
      fetch_env_var1()?
}

fn main() {
    let output = match example_function() {
        Ok(data) => { 
             println!("The env var1 value is: {:?}", data);
             data 
        }
        Err(e) => { 
           println!("Error retrieving env var1: {:?}", e);
           String::new()
        } 
    };

}

Basically, I keep getting stuck on if it is better practice to always handle errors from unwrap() at the lowest library level via a match statement OR is it better practice to always propagate errors up to the highest level of the function calls and handle them there?

Propagating errors is strictly more general. If you propagate errors, the caller can decide what to do with them. If you always do something with the error, then they can't poke into your function and override its behavior. So better not make decisions on behalf of the caller.

1 Like

Depends on the situation. In general, it's best to propagate errors. But if the function e.g. reads some configuration from the environment and there should be a default value, you should handle the missing environment error by returning the default value.

Note that the function may still need to be able to return an error, e.g. if the environment variable exists but contains a value that fails to parse as what it should be (e.g. a port number or a timeout duration etc). So back in the general case, your function should handle expected "errors" that has a natural expected result, but if there are anything else that can go wrong your function should return a Result. If your function calls anything that returns a Result in a way that you are convinced will never actually return an Err (e.g. compiling a hardcoded regex) it is fine to .unwrap() it.

1 Like