How do you early return an error in branches?

fn main() -> Result<(), Error> {
    match 1 {
        1 => Err("early return an error via ?")?,
        2 => return Err("early return an error via return + ?")?,
        3 => return Err("early return an error via return + .into()".into()),
        _ => (),
    }
    Ok(())
}

type Error = Box<dyn std::error::Error>;

Rust Playground

  • Err("early return an error")?
  • return Err("early return an error")?
  • return Err("early return an error".into())
0 voters

Option 2 is the worst of them all. I'd say it's obfuscation-level code. The return statement is a lie, since the ? operator short-circuits anyway and does its own kind of return, with type conversion, rather than a direct return implied by the keyword.

14 Likes

If you're making a macro to wrap this, then 2 is the best choice because of some silly details of tyvar fallback.

In any other situation, then I agree with @afetisov that 2 is a bad choice.

4 Likes

bail! :wink:

3 Likes

Yeah. Even if I don't use any error handling crate for a simple project, when I write too many return Err(format!(...).into()), I really want a bail! to save my day.

bail!(some_error)

is convenient, but it doesn't save much, compared to

return Err(some_error)

and what's more, there's no single bail!() but many different bail!()s and they all yields different types! it's ok if what you want is Result<T, anyhow::Error>, Result<T, error_stack::Report<E>>, Result<T, eyre::Report>, or others (failure, miette, etc).

it might save your a few characters when you want to bail!() with formatted message, but I don't like "stringly-typed" errors anyway. and what if the return type is Result<T, MyError>. where MyError may be deriving thiserror::Error, or it may be deriving snafu::Snafu, or it may just be manually crafted.

one example I find interesting is snafu, which doesn't have bail!(), instead it has whatever!() to return early with Result<T, snafu::Whatever> (Whatever is a type specially used for string errors). for non-string errors, you just use return Err(some_error). what's unique to snafu::whatever!() compared to other bail!() implementations is you can provide an optional source error to whatever!(), which can later be queried via std::error::Error::source(),

Actually, I've heard snafu for a while, but till now, its documentation is less clear, as with many error handling crates, for me. The most important part for me is what the error output is: the popular error handling crates rarely put their pretty errors in the first place, and I have to try a mini-code and see it. I've used anyhow/thiserror/eyre so far in different projects.

Like this:

use anyhow::*;
fn main() -> Result<()> { outer() }
fn inner() -> Result<()> { bail!("error from an inner funcfion") }
fn outer() -> Result<()> { inner() }

Error: error from an inner funcfion

anyhow demo: no context by default

use eyre::*;
fn main() -> Result<()> { outer() }
fn inner() -> Result<()> { bail!("error from an inner function") }
fn outer() -> Result<()> { inner() }

Error: error from an inner function

Location:
    src/main.rs:12:5

eyre demo: with source code location by default

use snafu::*;
fn main() -> Result<(), Whatever> { outer() }
fn inner() -> Result<(), Whatever> { whatever!("error from an inner function") }
fn outer() -> Result<(), Whatever> { inner() }

// with backtraces feature enabled, 
// if not, this will print `Error: Whatever { source: None, message: "error from an inner function", backtrace: Backtrace(()) }`
Error: Whatever { source: None, message: "error from an inner function", backtrace: Backtrace(   0: <snafu::backtrace_shim::Backtrace as snafu::GenerateImplicitData>::generate
             at .cargo/registry/src/index.crates.io-6f17d22bba15001f/snafu-0.7.5/src/backtrace_shim.rs:15:19
      <snafu::Whatever as snafu::FromString>::without_source
             at .cargo/registry/src/index.crates.io-6f17d22bba15001f/snafu-0.7.5/src/lib.rs:1464:17
   1: playground::inner
             at playground/src/main.rs:12:5
   2: playground::outer
             at playground/src/main.rs:16:5
   3: playground::main
             at playground/src/main.rs:8:5
   4: core::ops::function::FnOnce::call_once
             at /rustc/960754090acc9cdd2a5a57586f244c0fc712d26c/library/core/src/ops/function.rs:250:5
   ...

snafu demo: type-oriented, and the usage is more complicated, like no Result alias.

Personally I like to add custom bail! macros, if I'm working on at least a moderately complex project. You don't need to use stringly typed errors if you don't want to. Doesn't mean a custom macro can't save you some typing. For example, typically it's at least

return Err(some_error.into())

since you'd want to do some error conversion. You may also want to attach some extra context, like a backtrace, or the file and line where the error happened. If I'm writing a parser, I may want the errors to carry the line & column in the parsed text where the error happened. All of those cases require quite a bit more code than just return Err(Error).

I may want the errors to be strongly typed, but also to carry around some descriptive string which describes the exact error cause in human-readable terms. Sure, you may implement it via Display impls, but that would mean you'd have to have an error variant per each possible error location. What if a basically the same error can occur in slightly different contexts? A simple string may be sufficient to discriminate those errors, and the difference may be irrelevant from the calling functions' perspective.

Finally, most of the time the errors in my code are returned from assertion-style clauses. I like to introduce a custom ensure! macro and to write those assertions simply as

ensure!(condition, Error::Variant { .. });

It saves even more code, and makes it obvious that I'm just ensuring a few conditions, rather than implement more complex logic. At this point, bail! is just nice to have for consistency.

1 Like

maybe it's style preferences, but I do find snafu document is good to read, especially the guide part.

well, I guess you might like miette and tracing-error. to me, I mostly cares how to define and organize error types efficiently and conveniently.

miette is an exception in this regards, their documents put a fancy screenshot in the first paragraph. looks very cool I'd say.

I use snafu more like thiserror than anyhow, that is, I mainly use snafu to derive Display and std::error::Error for my own error type. for the user, they don't need to know it's derived using snafu or thiserror. Whatever is just an bonus feature so I don't need to switch between thiserror and anyhow.

whatever!() macro can be used with an inner Result, in which case it short circuits (somewhat like the old try!() macro or the question mark operator now), like this:

fn main() -> Result<(), Whatever> {
	// snafu::ResultExt::whatever_context() returns a Result<T, Whatever>
	// notice the question mark
	outer().whatever_context("context added in `main`")?;
	Ok(())
}
#[derive(Debug, snafu::Snafu)]
#[snafu(display("error from an inner function: {meaning_of_life}"))]
struct InnerError {
	meaning_of_life: i32,
	backtrace: Backtrace,
}
fn inner() -> Result<(), InnerError> {
	InnerSnafu { meaning_of_life: 42, } .fail()
}
fn outer() -> Result<(), Whatever> {
	// whatever!() macro short circuits and no question mark here
	let inner_return_value = whatever!(inner(), "context added in `outer`: {}", -42);
	Ok(inner_return_value)
}

one of the differences between whatever!() and anyhow::bail!() is that Whatever has a backtrace and an optional source chain, while anyhow::bail!() or anyhow::anyhow!() is just a string.

however, the backtrace can be very cluttered if there are multiple Whatevers in the source chain, just like how my example code adds context both in main and outer. if you run this code with backtraces feature on, the output is horrible to read, because backtraces are captured 3 times at different locations.

if you use snafu::Report as the main function's return type (or annotate fn main() with #[snafu::report] attribute), the source chain will be printed like this:

#[snafu(report)]
fn main() -> Result<(), Whatever> {
    //...
}
// output like this:
/*
context added in `main`

Caused by these errors (recent errors listed first):
  1: context added in `outer`: -42
  2: error from an inner function: 42
*/
1 Like

of course you can bail!(), if you are using anyhow, there's already anyhow::bail!() for you. if you are using failure, there's already failure::bail!(). error-stack, error-chain, miette, eyre, what have you, they all have a custom bail!(). I'm not saying bail!() is bad, I was just pointing out they lock you down to their special error types. they are not available when I'm using custom error types (think thiserror or snafu).

when I'm converting error types, it's most likely I'm wrapping a error returned from another function, in which case I'd use question mark (if From is not directly available, I would probably use something like anyhow::ResultExt::context or snafu::ResultExt::context to do the conversion). I think bail!() was most suitable to "generate" the root cause of an error chain, not for capture the intermediate error contexts.

sure, but these features don't needed be coupled with an early returning macro, right? e.g. using snafu, capturing a stack trace is as simple as add a field backtrace to the error type. same as source code location, just add a field location to the error type.

#[derive(Debug, Snafu)]
struct ParseError {
    // this is location where the error is constructed
    location: snafu::Location,
    // will automatically capture a stacktrace
    backtrace: snafu::Backtrace,
    // this is the source error
    source: SyntaxError,
    // custom context
    line: i32,
    column: i32,
}

#[derive(Debug, Snafu)]
enum SyntaxError {
    Eof,
    UpperCase,
    //...
}

fn parse() -> Result<T, ParseError> {
    // location, source, stacktrace are all handled automatically
    // only custom contexts needs to be specified
    let ast = check_syntax().context(ErrorSnafu {
        line: 42,
        column: 68,
    })?;
    todo!("do something")
}

again, this has nothing to do with a short-circuiting or early-returning bail!(), it's a error wrapping/context feature, and different libraries have different opinion for the solution. for example, using error-stack (with the help of snafu to derive Display and Error), it's as simple as:

#[derive(snafu::Snafu)]
struct MainError;

fn main() -> Result<(), error_stack::Report<MainError>> {
    let x = foo().change_context(MainError).attach("foo returned error, I don't know why")?;
    let y = bar().change_context(MainError).attach("bar also failed. I'm lazy to figure out why")?;
    Ok(())
}

no need to bail!(), just construct your error type and use question mark to short circuit.

yes, and ensure!() is indeed in almost every error handling crate I know of.

yeah, sure, as they are mostly already provided anyway. but "nice to have" is still not a strong argument to me.

1 Like