Rusty equivalent of C-style error checking?

After reading the error handling section in the Rust book I'm still having trouble wrapping my head around how to implement the Rust equivalent of certain C-style error checking. I think seeing the following C code converted into Rust (using Rust best practices) would be immensely helpful! Please avoid answers that involve heap allocation (e.g. Boxes) since embedded development generally doesn't allow for it.

const int32_t OK = 0;
const int32_t ERROR = -1;

int32_t operation1()
{
    int32_t status = foo();
    if (status != OK)
    {
        printf("Operation 1 failed\n");
    }
    
    return status;
}

void main()
{
    int32_t status = operation1();
    if (status == OK)
    {
        status = operation2();
    }

    if (status == OK)
    {
        printf("All operations successful\n");
    }
    else
    {
        printf("Some operation failed!\n");
        handle_errors();
    }
}

Your example never really uses the result of any operations so it may be a bit too contrived to show proper error handling in Rust, but I'll give it a shot.

In Rust you usually try to provide more information about an error than just "it failed", so it's quite common to create an error type containing contextual information about what went wrong.

For example:

#[derive(Debug)] // Note: derive automatically generates a way to print this to the screen
enum MyError {
  /// An error from the operating system (e.g. file not found)
  Io(std::io::Error),
  SomethingWentWrong {
    msg: String,
  },
}

Also, you know how you always need to remember to check the error code before accessing the result of an operation in C? Well Rust uses the type system to make sure it's impossible to access the result without doing the check. This is done using the Result (something which is either a successful outcome with a result, or a failed outcome with an error).

You use pattern matching to deal with either the success or failure, it's analogous to switching on status.

fn fallible_operation() -> Result<u32, MyError > {
  unimplemented!()
}

fn do_something_and_log_error() {
  match fallible_operation() {
    Ok(n) => println!("Got {}", n),
    Err(e) => println!("An error occurred: {:?}", e),
  }
}

fn add_one_or_propagate_errors_up() -> Result<u32, MyError > {
  match fallible_operation() {
    Ok(n) => Ok(n + 1)
    Err(e) => Err(e),
  }
}

Because doing something with the "success" variant from a result is such a common operation, Result has a map() method which will let you transform the success value.

fn add_one_or_propagate_errors_up_with_map() -> Result<u32, MyError > {
  fallible_operation().map(|n| n + 1)
}

Another thing is that idiomatic Rust prefers to return early when something goes wrong instead of adding loads of if (!failed) { ... } blocks. One way to do this is with explicit match blocks.

fn add_result_of_two_fallible_operations() -> Result<u32, MyError > {
  let first = match fallible_operation() {
    Ok(n) => n,
    Err(e) => return Err(e),
  };
  let second = match fallible_operation() {
    Ok(n) => n,
    Err(e) => return Err(e),
  };

  Ok(first + second)
}

However, it turns out matching on a result and returning if there is an error is so common that the ? operator was introduced. This lets us clean up add_result_of_two_fallible_operations().

It may seem like a trivial thing, but this killer feature lets you still explicitly handle errors while not cluttering the code with if error { ... } checks.

fn add_result_of_two_fallible_operations() -> Result<u32, MyError > {
  let first = fallible_operation()?;
  let second = fallible_operation()?;

  Ok(first + second)
}

You can even use the ? operator to return an error from main(). If that happens, the _start function automatically generated by the compiler will print an error message and set the return code appropriately.

fn main() -> Result<(), RandomNumberError > {
  let a= add_two_random_numbers()?;
  let b= add_two_random_numbers()?;

  println!("All operation successful");
  Ok(())
}

If the error handling logic is non-trivial you'll usually write an explicit match and inspect the error to figure out what went wrong.

7 Likes

Excellent answer, that cleared things up. Thanks!