Errors indicating bugs

In C++, exceptions are sometimes used to indicate an error that is due to a programming bug. In many situations no exception is produced, and instead the invalid use of an API is simply undefined behavior.

In Rust, panic! is used for these kinds of errors, often via an expect or unwrap method on Result or Option or via assertions like assert!. While a panic in Rust may unwind the stack or abort a program, it is never undefined behavior.

#include <cstddef>
#include <vector>

int main() {
  std::vector<int> v{1, 2, 3};
  // undefined behavior!
  int x(v[4]);
}
fn main() {
    let v = vec![1,2,3];
    // panics!
    let x = v[4];
}

Converting Result or Option to panic!

It is easier to convert from a Result or Option to a panic than to go the other way around. Therefore, many libraries in Rust are written to return Result or Option and allow the caller to determine whether a None result indicates a bug by using unwrap or expect to extract the value, panicking if there isn't one.

/// Returns `None` if the number cannot be divided evenly.
fn divide_exact(dividend: i32, divisor: i32) -> Option<i32> {
    let quotient = dividend / divisor;
    if quotient * divisor == dividend {
        Some(quotient)
    } else {
        None
    }
}

// Returns `None` if the number cannot be divided by 2
fn divide_by_two_exact(dividend: i32) -> Option<i32> {
    // divide_exact returning None here isn't a bug
    divide_exact(dividend, 2)
}

fn main() {
    let res = divide_exact(10, 3); // Oops, a bug!
    let x = res.unwrap();
    // ...
}

When designing an API, if only one of a Result-based (or Option-based) or panicking interface is going to be offered, it is generally better to offer the Result-based interface. That way that the caller can choose to omit the pre-condition checks and handle the error instead or to panic because pre-conditions should have been met.

Assertions

In Rust, panics are also generated by assertions. Unlike assert in C++, The assert! family of macros in Rust cannot be disabled. They are therefore appropriate for asserting invariants when creating safe wrappers for unsafe code, in addition to checking for logical invariants.

#include <cassert>
#include <cstddef>

template <typename T>
class Widget {
  T *parts;
  std::size_t partCount;

public:
  // ... constructors ...

  /**
   * @pre n must be smaller than partCount
   */
  T getPart(std::size_t n) {
    // Unlike in Rust, this can be disabled,
    // e.g., with -DNDEBUG.
    assert(n < partCount);
    return *(parts + n);
  }
};
#![allow(unused)]
fn main() {
pub struct Widget<T> {
    parts: *const T,
    part_count: usize,
}

impl<T: Copy> Widget<T> {
    // ... constructor methods ...

    /// Panics if n is greater than the number of
    /// parts.
    pub fn get_part(&self, n: usize) -> T {
        // SAFETY: Widget maintians invariant of
        // at least part_count parts, so if n is
        // less than the part count then we can
        // use it access a part.
        assert!(
            n < self.part_count,
            "index {} exceeds part count {}",
            n,
            self.part_count
        );
        let idx = isize::try_from(n).expect(
            "can't convert index to offset"
        );
        unsafe { self.parts.offset(idx).read() }
    }
}
}

The Rust debug_assert! macro is more like assert! in C++, in that it can be turned off by a compilation configuration option, and so is useful for encoding logical invariants that should be checked during development and testing, but are too expensive to check in production.

Other assertion macros

Rust has several other convenience assertion macros. The macros assert_eq! and assert_ne! will print their arguments on assertion failure using the Debug trait implementation.

The unreachable! macro is for asserting that when matching on an enum certain cases are expected to not be possible. It is essentially panic! with a fixed error message, but better communicates intent.

Static assertions

C++ also has static_assert, which is guaranteed to be evaluated at compile time, other than when used in templates. When used in templates, it is guaranteed to be evaluated at compile time if the template is instantiated. In Rust the same thing can achieved by calling assert! in a const block or some other constant context. The convenience macros assert_eq! and assert_ne! cannot (yet) be used in const contexts.

The following example fails to compile in both Rust and C++ with the message from the static assertion.

#include <cassert>

int main() {
  static_assert(false, "static requirement");
}
fn main() {
    const {
        assert!(false, "static requirement");
    }
}

Like with C++ static_assert, a Rust assertion in a const block in a generic definition is only evaluated when the generic arguments are known. Both the C++ and the Rust versions of the following example only fail to compile if the first function is called on an array with a size less than 1.

#include <array>
#include <cassert>
#include <cstddef>

template <const std::size_t n>
int &first(std::array<int, n> arr) {
  static_assert(
      n >= 1,
      "array needs to have at last size 1!");
  return arr[0];
}
#![allow(unused)]
fn main() {
fn first<const N: usize>(arr: [i32; N]) -> i32 {
    const {
        assert!(
            N >= 1,
            "array needs to have at last size 1!"
        )
    }
    arr[0]
}
}

In C++, static_assert can also be used at namespace scope. To achieve an equivalent thing in Rust requires defining an unnamed constant.

static_assert(true,  "top-level assert true");
static_assert(false,  "top-level assert false");

int main() {}
const _: () = assert!(true, "top-level assert true");
const _: () = assert!(false, "top-level assert false");

fn main() {}

Assertions and the optimizer

Assertions do affect how the Rust compiler optimizes code (e.g., by enabling the optimizer to eliminate subsequent redundant checks) but the specific effects are not guaranteed.

Panics in embedded systems

When programming in Rust for embedded systems using #![no_std], there is no default panic handler. Instead one must be specified using the #[panic_handler] annotation.

The Embedded Rust Book chapter on handling panics has more details on implementing panic handlers for in no_std programs.