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. 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.
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")?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.
``` 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 suggestedThe 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.
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).
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.
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.
err := foo()
if err != nil {
return err
}No offense, but I think it's Stockholm Syndrome. It reminds me of what the JavaScript folks used to say 5 years ago: "No, it's not a terrible language. I love JavaScript's callback hell." Of course once it gained promises/await, now they _really_ love it.
I've been using Go for the last year and while I do genuinely enjoy the simplicity of the language, Go does have these annoying little customs like pebbles in your shoe.
You can _say_ it forces you to think about error handling, but I find it does not. To be honest, I'm able to not think about error handling just fine. It just forces me to write boilerplate when I'm trying to crank out a quick non-robust prototype.
The whole attitude with Kotlin is that exceptions are exceptional and that you probably won't be doing anything productive with them beyond logging. Using exceptions for conditional logic is a bit of a design smell. Sometimes you have to with some older Java libraries. But it's not what you are supposed to do. Otherwise, catch exceptions centrally. When using co-routines, uncaught exceptions actually cancel the co-routine scope (and the the other co-routines in them, if any), which gives you an opportunity to deal with that in a sane way. You can have error handlers attached to those to deal with that. Structured concurrency is a buzzword they like to use for this notion.
I've been on a few Go projects; never as a main developer but enough to understand what is happening. I appreciate its simplicity; it's easy to get into. That is IMHO the main value of go: getting stuff done. But the verbosity is a bit painful if you come from something higher level. With Kotlin's native compiler slowly getting usable, I could see myself opting for that where I might have used Go or something else in the past (e.g. command line tools).
When I'm writing Go, it returns where it says "return".
Beyond that, you don't need to care if the function will terminate early because of an exception - as long as all necessary cleanup is handled regardless of exit reason, you don't care that the function will finish early if an HTTP request fails before using the value of the request.
And usually you only want to catch and re-throw exceptions around module (in the broad sense) boundaries. If you catch and re-throw exceptions in every function, you're either losing lower-level context completely OR you're leaking implementation details anyway if you're chaining up the old error.
In Go, the error needs to be manually propagated through every function, with enough context to understand what's going on. In Java, the error needs to be manually handled at every module boundary; function-level context is given automatically by the call-stack.
The fact is that even code that does no resource acquiring could throw an exception. It might not even be a documented exception. And when it throws that exception that you may or may not have been expecting some code somewhere is going to have to figure out what to do. In java the only real way to communicate an error is to throw an exception. For some reason culturally Java has decided that it's too much work to distinguish between errors that should crash the app and errors that should be handled by the caller in some way. As a result when an exception outside of your code happens in production and you read the exception in your logs it will:
1. Not have a significant part of the information you need to debug the problem.
2. Be in code you may not have easy access to go read.
3. You will probably have no idea why it's happening.
Eventually for a long enough lived codebase you will have encountered and documented all of these in a runbook somewhere or you won't and everyone will have to have learned them over and over again.
If you are on an exceptional team you'll have caught and then wrapped or handled these exceptions to make operating the software in production less painful. But all of it could have been avoided if java had chosen a better path.
Make it impossible trap runtime exceptions. They just crash the program. Make trappable exceptions checked exceptions. The code would get more verbose. But the operational pain would have been vastly reduced.
Errors are a part of your API. Unchecked Runtime exceptions pretend like 50% of your API surface doesn't exist.
If these errors are things that the system administrator can handle, they will find them in the logs, the cause of the error will hopefully be clear enough if enough effort was spent on making it that way, and the sysadmin will fix the cause. If the error is something that the programmers never anticipated, nothing more can be done and a bug needs to be open with the developer. In some cases, even though the problem was with the environment, error reporting may have been fudged and the sysadmin may not be able to understand what they are suppose to fix - bad error reporting.
I fail to see how exceptions hurt with any of these cases. I have no more or less information about error cases seeing `func foo() error` or `void foo() throws Exception`, so neither helps me know what errors I should handle. It's no harder in Java to add context to errors when you really want/can - in fact, it's easier:
try{
stuff1();
stuff2();
} catch (Exception e) {
throw SpecificError("I was doing this when something else happened", e);
}
vs err = stuff1();
if err != nil {
return fmt.Errorf("I was doing this with stuff1 when something else happened: %w", err)
}
err = stuff2();
if err != nil {
return fmt.Errorf("I was doing this with stuff2 when something else happened: %w", err)
//note: different error message, since we're missing call stacks to know where this actually failed
}
Errors are part of your API - agreed. In C# or Java without checked exceptions, you have to assume that any function can throw any exception. In Go, you get exactly 1 more bit of information: a function tells you IF it returns an error or not - same as modern C++ with `noexcept`. But if you actually want to know what errors are returned, you're SOL in most languages (Java with checked exceptions helps, but causes other problems). And none of these languages helps you with adding context to errors, except the so so context of stack traces in C# and Java.I find that extra bit of information to be crucial you do not. I think you are perhaps discounting the value that Go allows you to add in your apis by making the error type explicit which then does tell you what error you might be getting. Go forces the programmer to make the fact that there is an error explicit which is good. If it also forced the developer to be explicit in which type of error it can return that would also be good since it would enforce API boundaries for errors. But I think Go is still ahead of the game compared to Java on this one.
Resources that need to be closed at function exit must be wrapped with `using` or `try/finally` in Java; with `defer` in Go.
Functions that can handle specific errors from a sub-function need to know about that error type in Java, error type or error value in Go.
Functions that can't handle any specific errors from their callees don't need to do anything in Java; they need to propagate any error in Go.
It's true that in Java it's less clear where a function can end, since any statement can potentially throw. This is technically true in Go as well with panic(), but let's accept that that is much more rarely used. But either way, if the function has any cleanup to do, that cleanup must be done in try/finally or defer, otherwise the function is brittle. So, why do I care if the function can throw an exception in the middle?
1. https://www.infoq.com/news/2019/07/go-try-proposal-rejected/
For example, this code:
void foo() {
var res = acquireResource();
useResource(res);
releaseResource(res);
}
is wrong even if no errors can be thrown. By contrast, this code is correct, regardless of whether any of these functions can throw: void foo() {
try {
var res = acquireResource();
useResource(res);
} finally {
releaseResource(res);
}
}
Similarly, in Go this function is wrong: func foo() error {
res, err := acquireResource()
if err != nil {
return fmt.Errorf("error acquiring resources while foo-ing: %w", err)
}
err = useResource(res)
if err != nil {
err = fmt.Errorf("error using resource while foo-ing: %w", err)
}
releaseErr := releaseResource(res)
if releaseErr != nil {
err = fmt.Errorf("%w; failed to releasing resource while foo-ing: %w", err, errW)
}
return err
}
even though I don't have any exceptions.The correct way to do this in Go would be:
func foo() error {
res, err := acquireResource()
if err != nil {
return fmt.Errorf("error acquiring resources while foo-ing: %w", err)
}
defer func() {
err := releaseResource(res)
if releaseErr != nil {
logInSomeWay(fmt.Errorf("failed to releasing resource while foo-ing: %w", err))
}
}()
err = useResource(res)
if err != nil {
return fmt.Errorf("error using resource while foo-ing: %w", err)
}
}
So overall, whether a function finishes early SHOULD be irrelevant.If you want to see what throws "FooException", you need an IDE to do it, unless you have the whole Javadoc memorised. It might be a hundred lines up and in the middle of a foo().bar().baz() chained series of method calls.
And yes, you need to know. The same exception in two different places might mean two very different things in terms of what actually broke, or what you should do to recover or fail the task at hand. Because of this, bubbling up raw exceptions out of deeply nested functions is generally a code smell, because by the time they get to the top level of even a module, all you know is "it broke". Unless you did the donkey work of wrapping the raw exception in something with more semantic context. The same work you'd do in Go.
This isn't any different in Go. Tracing Exceptions is actually often easier since they have unique names that you can grep for, whereas err is always err, so your only recourse is to trace across the callstack.
It's, like, actually worse.
> Because of this, bubbling up raw exceptions out of deeply nested functions is generally a code smell, because by the time they get to the top level of even a module, all you know is "it broke". Unless you did the donkey work of wrapping the raw exception in something with more semantic context.
What? If you have a decent root level error message, that + a stack trace is almost always enough, and is as good or better than what you get in go, since the errors are more structured. I'd generally consider the donkey work you're describing an antipattern. Adding extra context or re-raising an exception should be done only rarely, when you have confidence that the new context you're providing is more useful than the prior (and lots of good languages support exception chaining so you get all of the context from many errors, instead of just one string).
That's a completely different problem, and one that is not unique to errors or exceptions. For any polymorphic type where you want to handle different variants differently, you need to know the possible variants, and this information is, by definition, not present in the code sending the polymorphic value. This is true whether you want to handle exceptions in C++, Optional t in Haskell, or interface values in Go.
> Because of this, bubbling up raw exceptions out of deeply nested functions is generally a code smell, because by the time they get to the top level of even a module, all you know is "it broke". Unless you did the donkey work of wrapping the raw exception in something with more semantic context.
Wrapping up exceptions at every function is a huge code smell. You sometimes want some level of wrapping, especially around module boundaries or in functions with highly relevant context along the way, but generally most functions along a call stack don't have anything to add to a lower level exception - especially if the lower-level library is well made. For example, fs.PathError in Go and System.IO.FileNotFoundException in C# include a way to programatically find the name of the file that was not found, so even if thrown from a low level, a higher level still has the most relevant context available without wrapping. In contrast, java.io.FileNotFoundException does not include this information unless you parse the error string, so you would have a reason to wrap it with more relevant context.
Does that seem reasonably representative? To a non-Go programmer, it doesn't seem that bad, but it sure does have a lot of error-handling boilerplate -- squinting at it, something around 50% of the non-comment lines? It just seems kind of weird, if you have to write "err != nil" and "return nil, err" over and over, not to have any syntactic sugar for it!
There's what looks like missing return values on lines 64 and 69, then down on line 93 there's an explicit "return // ignore I/O error".
Is this file outdated, or otherwise not a good example? I used Google to find it, and it says "Copyright 2020" in the header.
The missing return values on 64 and 69 are the "Named Return Values" thing that OP mentions - the returns are the variables named in the function declaration.
Some HN comments toward Go are a bit baffling.
Whereas when I use Rust's Result types, I tend to not add any additional context with `map_err` (until I need to investigate the cause of an error and have to retrofit it).
There's still definite room for improvement though (I think the Go 2 proposal will help a lot).
Sum types and pattern matching to enforce that would be even bettern. What I don't like about Go error handling isn't that it's "verbose" but that it's primitive.
So if you think Data first, Go is great. If you think in terms of method chaining and verbs or nouns first, then Go won't treat you kindly.