fn my_func() -> Result<(), Error> {
foo()?;
bar()?;
baz()?;
Ok(())
}
// ---
let val = bar()?;
println!("{}", val);
// ---
fn my_func() -> Result<i32, Error> {
foo()?;
bar()?;
let val = baz()?;
// ... more stuff
Ok(val)
}
// ---
fn my_func() -> Result<(i32, String), Error> {
foo()?;
bar()?;
let (val1, val2) = baz()?;
Ok((val1, val2))
// Those last two lines could even be just `baz()` or `Ok(baz()?)`.
}
Rather close to the world the author wants to live in.``` let (val1, val2) = baz() .map_err(|err| add_my_context(err, "some context"))?; ```
Also, the `?` operator automatically calls `into()` on the error that it was given, which will convert it into the desired error type (assuming there's a conversion implemented for it), so you only need the `map_err()` approach when you need to add context.
I'm not sure what the idiomatic Rust way of logging before returning the error is - in the code I've written (and read), you'd add return the error with added context and leave the handler to decide whether to log.
In my experience, it is pretty ergonomic.
let foo = foo()?;
// Expanded
let foo = match foo() {
Ok(v) => v,
Err(e) => return Err(From::from(e))
};
If you're using your own error type then you can impl From/Into yourself to capture additional context from the source error (or a stacktrace) in addition to using map_err() as others have suggested foo().map_err(|err| WrapperError::new(err))?
For context, typically you would use a library like `anyhow` which allows you to do: foo().context("Context string here")?
or foo().with_context(|| format!("dyanmically generated context {}", bar))?
I don't know of any pre-built solutions for logging, but you could easily add a method to Result yourself that logged like: foo().log("log message")?
or something like: foo().log(logger, 'info', "log message")?
The code to do this would be something like: use log::error;
trait LogError {
fn log(self, message: impl Display) -> Self;
}
impl<T, E> LogError for Result<T, E> {
fn log(self, message: impl Display) -> Self {
error!("{}", message);
self
}
}
Then you'd just need to import the `LogError` error trait at the top of your file to make the `.log()` method available on errors.Is chaining+traits+wrappers+map_err truly less complex than a conditional:
if BAD {
Log()
Metric1()
Metric2()
}Having said that, if you're outputting the same metrics for every error then a method that does this for you makes a lot more sense than repeating yourself all over the place.
Remember also that .map_err() need not be significantly different from that if block; it can be essentially just a slightly different spelling:
let data = some_call().map_err(|e| {
log();
metric1();
metric2();
e
})?;
But I would draw your attention in all this to the fact that you’re dealing with algebraic data types, enums with data attached to the variants; this isn’t just some nullable data value and nullable error where you can say `if error { … }`; in order to get at the data, you need to handle the error. This has profound implications that, once you’re used to it, will shape your coding style and techniques even when you return to other languages.On things like metrics, I’d also mention that sometimes you may be better served by an RAII pattern, which can be made to be the rough equivalent of a defer statement in Go. Especially if you’re pairing start/end metrics, RAII can be particularly good at that.
With something like a functional effect system you can even do better than that. For instance with Scala ZIO you can do something like:
``` val result = for { fooResult <- foo().tapError(e => console.putStrLn(s"Error in foo!: $e") barResult <- bar(fooResult).tapError(e => console.putStrLn(s"Error in bar!: $e") bazResult <- baz(barResult) } yield bazResult ```
Then when you run the bazResult effect you get either an error or the result type which you can handle however you like.
match foo() {
Ok(val) => val,
Err(err) => return Err(err.into()),
};
(This isn’t exactly how it’s implemented—that detail is unstable and has actually changed <https://github.com/rust-lang/rfcs/pull/3058> since ? was stabilised, without disrupting things!—but it’s equivalent in this instance. As examples of how it’s not exact: you can use ? on Option too; and inside a try block (unstable), ? isn’t a return, acting more like a break-with-value.)For wrapping the error: note the .into() in the desugaring: it’ll perform any necessary type conversions which is the normal way you’d do error wrapping. But if you want to do anything more than that, or adding specific context, you’re still in luck! You can use method chaining to manipulate the result. In the standard library is map_err <https://doc.rust-lang.org/std/result/enum.Result.html#method...>:
foo().map_err(|err| f(err))?
And you can add other methods to such types with extension traits; as an example, the anyhow crate, popular for application-level error handling (where you don’t care about precise modelling of errors) makes it so that, on importing anyhow::Context <https://docs.rs/anyhow/1.0.44/anyhow/trait.Context.html>, you can add context to an error like this: foo().context("foo failed")?
foo().with_context(|| "foo failed")?It’s up to the caller what to do with an error - panic, return it in turn, wrap it, or whatever else makes sense in context. It’s semantically quite similar to Go - except with better library support (Result instead of returning a tuple) and better syntax (the “?” operator instead of manual checks).
The question mark operator just gives you an easy way of doing perhaps the most common handling of errors, which is propagating it up to the caller to worry about. So somewhere along the way you’re going to need to actually handle it (or explicitly ignore it), though it’s also possible in simple programs to let Rust handle it, by having your main function return a Result (that is, something like `fn main() -> Result<(), Error>`), in which case if it’s an Err it prints the error (roughly `eprintln!("Error: {:?}", err);`) and exits with status code 1.
For truly exceptional situations, Exceptions are a good match because actually you wanted a control flow change. If the NetworkFailed maybe all of this complicated multi-node-synchronisation code is useless and we should handle that in code for NetworkFailedExceptions.
But one program's exceptional situation is another's business as usual, our desktop network visualisation icon does not want to eat a NetworkFailedException, the fact that NetworkFailed is just a reason to change the little icon to red instead of green, not exceptional at all.
This is particularly egregious when doing large data processing. If I doBackups() sending forty different sources to six backup servers, I actually don't want the failure of one backup server to handle one of the forty sources to blow up the entire function, I would much rather get back a detailed overview and go OK, 5 out of 6 ain't bad, no need to set off alarm bells and wake up the sysadmins. Error handling Go's way makes this a bit ugly but practical, Rust's way makes it feel reasonably ergonomic, in languages like C++ or Java it was kinda horrible (but C++ is slated to get an Expected class to mostly fix this in C++ 23).
Having a Result type with statically enforced checking sounds good though.
err := foo()
if err != nil {
return err
}No. `?` performs an optional conversion and an early return, that’s it (kinda).
> If not, how do you know where the error has come from?
By default you don’t. There are utility libraries you can use to attach such information to error types (and eventually the stdlib will provide one), but OOTB an error value is… quite literally anything (it should probably implement `Error` but doesn’t actually have to).
This is in keeping with the idea that hidden costs should be avoided when possible, and that reified results should be the norm so should have as little requirement as possible.
It depends. `?` will convert the inner error type (the value from the function to its left) to the outer error type (the return type of the function containing this line). That conversion function can be defined by the inner error type. If the conversion generates a stack trace (or adds to an existing one), then there will be one.
Therefore, "Is there always a stack trace available?" is a tricky question-- it was seen as unimportant during the 1.0 days because they wanted to be able to accommodate low-resource environments.
Today, there's third party error handling libraries that cut out error creation / manipulation boilerplate, that might create stack traces for you, and there's efforts ongoing to see how it might be added to the stdlib.