4 ms·
What happens if you want to log something before returning the error, or wrap the error, add some context or something ? Is there a way to override the ? operat
by thiht 5y ago
What happens if you want to log something before returning the error, or wrap the error, add some context or something ? Is there a way to override the ? operator? Or do you fallback to a matcher, which is roughly the same as Go error handling?
- maffydub 5y agoThe `Result` type has a `map_err` method to which you can pass a function (or closure) to modify the error, so you can write something like... ``` 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.
- brundolf 5y agoYou can .map_err() before the ? to transform it inline using a closure
- ccakes 5y agoThe operator is just syntax sugar for something roughly this 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
- nicoburns 5y agoIf you want to manually wrap/change the error somehow then the pattern would be something like: 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.
- samhw 5y agoNice to see so many people mentioning `map_err`. This is exactly what I've done when working with Rust (I have professional experience with both, slightly more with Go) and it's a dream compared with Go's error handling.
- politician 5y agoWhat if you want to log, then emit some time series data? Is chaining+traits+wrappers+map_err truly less complex than a conditional: if BAD { Log() Metric1() Metric2() }
- nicoburns 5y agoIf you're doing something more complex/involved in order to handle a particular error, then no I think a conditional/match is fine. It's the common case when you're not doing any special that the boilerplate becomes overwhelming. 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.
- chrismorgan 5y agoAnd this is the beauty of chaining and extension traits: you can define your own “shorthands” without much trouble, because it’s all in the normal type system. 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.
- deleted 5y ago[deleted]
- thinkharderdev 5y agoYeah, if you need to explicitly do something with the error then you fall back to a matcher. But the idea is that you only need to deal with errors at error boundaries which you can define. So if you just want to pass an error up the stack to be handled somewhere else then you can with almost no syntactic overhead. 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.
- chrismorgan 5y agoIn the code shown, `foo()?;` is equivalent to this: 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 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.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 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")?