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::Resultiscu::Resultanyhow::bail!iscu::bail!anyhow::Okiscu::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 tobail!, but works with anErrorinstance at handcu::unimplemented!andcu::unreachable!that are similar to the std macros, but instead ofpanic!, they willbail!cu::ensure!is an assert-like check for invariants that should almost never fail if the program is correct (i.e. a debugging aid, likeassert!, but returns an error instead of panicking). Unlikeanyhow::ensure, it evaluates to aResult<()>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 - usecheck!orbail!for those.cu::some!checks anOptionand returnsOk(None)if the option isNone.
Here are other anyhow re-exports that are less commonly used
anyhow::anyhowiscu::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.