Skip to main content

check

Macro check 

Source
macro_rules! check {
    ($result:expr, $($args:tt)*) => { ... };
}
Expand description

§Error Handling

Does not require any feature flag. Please make sure to sponsor David Tolnay if you depend heavily on his work on the Rust ecosystem.

Most of the error handling stuff is re-exported from anyhow, which is a crate that makes tracing and formatting error messages SUPER easy. This is the easiest way to quickly write debuggable program without structured error. Structured error types would be more useful if you are making a library though.

The traits required for error handling are included in the prelude import

use cu::pre::*;

Here are the most commonly used anyhow re-exports

  • anyhow::Result is cu::Result
  • anyhow::bail! is cu::bail!
  • anyhow::Ok is cu::Ok

Here are custom utilities from cu that integrates with anyhow

  • cu::check! wraps .with_context(). This is the general-purpose way to add context to errors that can happen at runtime even when the program is correct, such as invalid user input or system errors (e.g. file does not exist)
    use cu::pre::*;
    
    fn some_fallable_func() -> cu::Result<String> {
        Ok("foo".to_string())
    }
    fn main() -> cu::Result<()> {
        // this input is just to show the formatting
        let input: i32 = 42;
    
        let foo = cu::check!(some_fallable_func(), "failed: {input}")?;
        // with anyhow, this would be:
        // let foo = some_fallable_func().with_context(|| format!("failed: {input}"))?;
        // -- much longer!
        assert_eq!(foo, "foo");
        Ok(())
    }
  • cu::rethrow! is similar to bail!, but works with an Error instance at hand
  • cu::unimplemented! and cu::unreachable! that are similar to the std macros, but instead of panic!, they will bail!
  • cu::ensure! is an assert-like check for invariants that should almost never fail if the program is correct (i.e. a debugging aid, like assert!, but returns an error instead of panicking). Unlike anyhow::ensure, it evaluates to a Result<()> instead of generates a return. It also does not automatically generate debug information. Do not use it to validate user input or check for system errors - use check! or bail! for those.
  • cu::some! checks an Option and returns Ok(None) if the option is None.

Here are other anyhow re-exports that are less commonly used

  • anyhow::anyhow is cu::fmterr

Finally, if you do need to panic, cu::panicand allows you to also log the same message so you can debug it easier.

§Context (To check or not to check)

It is tricky to determine if you should wrap the result with a check!, or just propagate it with a ?. Ultimately, there is no correct answer (you may say it is contextual). Another way of phrasing the same question is if the context should be added by the caller or the callee.

The principle I personally follow is the caller should only check! if there are additional information in the caller’s context that the callee does not already know. The information that both the callee and caller have access to is:

  • The function parameters
  • The name of the call
  • The behavior of the function

These make up the API of the function.

For example, I will write the following code

use cu::pre::*;

fn process_paths(paths: &[&str]) -> cu::Result<()> {
    cu::info!("processing paths...");
    for (i, path) in paths.iter().enumerate() {
        // here there might be some candidates for context:
        // - "failed to save important value to '{path}'"
        //   - the callee knows the current context is saving
        //     "important value" to "path" (from function name and parameter),
        //     therefore this message does not add additional context
        // - "process paths failed on '{path}'"
        //   - this could work in some cases, but here I know
        //     cu would already log the path if it fails
        // - here I choose to log {i} which might help me finding the erroreous
        //   path from some kind of data set.
        cu::check!(save_important_value_to(path), "failed to process {i}th path")?;
    }
    Ok(())
}

fn save_important_value_to(path: &str) -> cu::Result<()> {
    // here, the only information I have that cu::fs::write
    // does not have, is "important value" is "some random thing".
    // this is not an important context to log to the error,
    // so I choose to simply ?
    cu::fs::write(path, "some random thing")?;
    Ok(())
}

With that said, now we can introduce cu::context, which wraps a function and append a formatted context to it. This is a double-edge sword. You could end up with unnecessarily bloated error stack if you put too much context. However, it can be extremely useful in a complex function with many possible error paths to add context for what the overall failure is.

If you find yourself writing the same check! to every invocation of some function. Considering using it. However, I would not use this on any public API of your code.