Error and None Propagation

We should use panics like panic!(), unwrap(), expect() only if we can not handle the situation in a better way. Also if a function contains expressions which can produce either None or Err,

  • we can handle them inside the same function. Or,
  • we can return None and Err types immediately to the caller. So the caller can decide how to handle them.

💡 None types no need to handle by the caller of the function always. But Rusts’ convention to handle Err types is, return them immediately to the caller to give more control to the caller to decide how to handle them.

? Operator

  • If an Option type has Some value or a Result type has a Ok value, the value inside them passes to the next step.
  • If the Option type has None value or the Result type has Err value, return them immediately to the caller of the function.

Example with Option type,

fn main() {
    if complex_function().is_none() {
        println!("X not exists!");
    }
}

fn complex_function() -> Option<&'static str> {
    let x = get_an_optional_value()?; // if None, returns immediately; if Some("abc"), set x to "abc"

    // some other code, ex
    println!("{}", x); // "abc" ; if you change line 19 `false` to `true` 

    Some("")
}

fn get_an_optional_value() -> Option<&'static str> {

    //if the optional value is not empty
    if false {
        return Some("abc");
    }
    
    //else
    None
}

Example with Result Type,

fn main() {
    // `main` function is the caller of `complex_function` function
    // So we handle errors of complex_function(), inside main()
    if complex_function().is_err() {
        println!("Can not calculate X!");
    }
}

fn complex_function() -> Result<u64, String> {
    let x = function_with_error()?; // if Err, returns immediately; if Ok(255), set x to 255

    // some other code, ex
    println!("{}", x); // 255 ; if you change line 20 `true` to `false`

    Ok(0)
}

fn function_with_error() -> Result<u64, String> {
    //if error happens
    if true {
        return Err("some message".to_string());
    }

    // else, return valid output
    Ok(255)
}

try!()

? operator was added in Rust version 1.13. try!() macro is the old way to propagate errors before that. So we should avoid using this now.

  • If a Result type has Ok value, the value inside it passes to the next step. If it has Err value, returns it immediately to the caller of the function.
// using `?`
let x = function_with_error()?; // if Err, returns immediately; if Ok(255), set x to 255

// using `try!()`
let x = try!(function_with_error());

Error propagation from main()

Before Rust version 1.26, we couldn’t propagate Result and Option types from the main() function. But now, we can propagate Result types from the main() function and it prints the Debug representation of the Err.

💡 We are going to discuss about Debug representations under Error trait section.

use std::fs::File;

fn main() -> std::io::Result<()> {
    let _ = File::open("not-existing-file.txt")?;

    Ok(()) // Because of the default return value of Rust functions is an empty tuple/ ()
}

// Because of the program can not find not-existing-file.txt , it produces,
//    Err(Os { code: 2, kind: NotFound, message: "No such file or directory" })
// While propagating error, the program prints,
//    Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }

💯 If you want to know about the all kind of errors std::fs::File::open() can produce, check the error list on std::fs::OpenOptions.