f = open('foo.txt', 'w')
is even more succinct, and the exception thrown on failure will not only contain the reason, but the filename and the whole backtrace to the line where the error occurred. try:
f = open('foo.txt', 'w')
except Exception as e:
raise NecessaryContext("important information") from e
Else your callers are in for a nightmare of a time trying to figure out why an exception was thrown and what to do with it. Worse, you risk leaking implementation details that the caller comes to depend on which will also make your own life miserable in the future.The exceptions from something like open are always pretty clear. Like, the files not found, and here is the exact line of code and the entire call stack. what else do you want to know to debug?
Look, if you're just writing a script that doesn't care about failure — where when something goes wrong you can exit and let the end user deal with whatever the fault was, you don't have to worry about this. But Go is quite explicitly intended to be a systems language, not a scripting language. That shit doesn't fly in systems.
While you can, of course, write systems in Python, it is intended to be a scripting language, so I understand where you are coming from thinking in terms of scripts, but it doesn't exactly fit the rest of the discussion that is about systems.
That doesn't make sense. Go errors provide exactly whatever information is relevant to the error. The error type is an interface for good reason. The only limiting bound on the information that can be provided is by what the computer can hold at the hardware level.
> They might as well be lists of strings.
If a string is all your error is, you're doing something horribly wrong.
Or, at very least, are trying to shoehorn Go into scripting tasks, of which it is not ideally suited for. That's what Python is for! Python was decidedly intended for scripting. Different tools for different jobs.
Go was never designed to be a scripting language. But should you, for some odd reason, find a reason to use in that that capacity, you should at least being using its exception handlers (panic/recover) to find some semblance of scripting sensibility. The features are there to use.
Which does seem to be the source of your confusion. You still seem hung up on thinking that we're talking about scripting. But clearly that's not true. Like before, if we were, we'd be looking at using Go's exception handlers like a scripting language, not the patterns it uses for systems. These are very different types of software with very different needs. You cannot reasonably conflate them.
The error type in go is literally just a string
type error interface { Error() string }
That's the whole thing.
So i dont know what your talking about then.
The wrapped error is a list of error types. Which all include a string for display. Displaying an error is how you get that information to the user.
If you implement your own error, and check it with some runtime type assertion, you have the same problem you described in python. Its a runtime check, the API your relying on in whatever library can change the error returned and your code won't work anymore. The same fragile situation you say exists in python. Now you have even less information, theres no caller info.
No, like I said before, it's literally an interface. Hell, your next line even proves it. If it were a string, it would be defined as:
type error string
But as you've pointed out yourself, that's not its definition at all.> So i dont know what your talking about then.
I guess that's what happens when you don't even have a basic understanding of programming. Errors are intended to be complex types; to capture all the relevant information that pertains to the error. https://go.dev/play/p/MhQY_6eT1Ir At very least a sentinel value. If your error is just a string, you're doing something horribly wrong — or, charitably, trying to shoehorn Go into scripting tasks. But in that case you'd use Go's exception handlers, which bundles the stack trace and all alongside the string, so... However, if your workload is script in nature, why not just use Python? That's what it was designed for. Different tools for different jobs.
The cherry on top is that you always have a place to add context, but it's not the main point.
In the Python example, anything can fail anywhere. Exceptions can be thrown from deep inside libraries inside libraries and there's no good way to write code that exhaustively handles errors ahead of time. Instead you get whack-a-mole at runtime.
In Go, at least you know where things will fail. It's the poor man's impl of error enumeration, but you at least have it. The error that lib.foo() returned might be the dumbest error in the world (it's the string "oops") but you know lib.foo() would error, and that's more information you have ahead of time than in Python.
In Rust or, idk, Elm, you can do something even better and unify all downstream errors into an exhaustive AGDT like RequestError = NetworkError(A | B | C) | StreamError(D | E) | ParseError(F | G) | FooError, where ABCDEFG are themselves downstream error types from underlying libraries/fns that the request function calls.
Now the callsite of `let result = request("example.com")` can have perfect foresight into all failures.
exceptions vs returned errors i think is a different discussion then what im getting at here.
Stack traces are reserved for crashes where you didn't handle the issue properly, so you get technical info of what broke and where, but no info on what happened and why it did fail like it did.
You can get away with not doing that when cowboy coding scripts. Python was designed to be a scripting language, so it is understandable that in Python you don't often need to worry about it. But Go isn't a scripting language. It was quite explicitly created to be a systems language. Scripts and systems are very different types of software with very different needs and requirements. If you are stuck thinking in terms of what is appropriate for scripting, you're straight up not participating in the same thread.
The Go team actually did a study on exactly that; including stack traces with errors. Like you, they initially thought it would be useful (hence the study), but in the end, when the data was in, they discovered nobody ever actually used them. Meaningful errors proved to be far more useful.
Science demands replication, so if your study disagrees, let's see it. But in the absence of that, the Go study is the best we've got and it completely contradicts what you are telling us. Making random claims up on the spot based on arbitrary feelings isn't indicative of anything.
That said, I think we can all agree there is a limited place for that type of thing (although in that place you shouldn't use Go at all — there are many other languages much better suited to that type of problem space), but in that place if you had to use Go for some strange reason you'd use panic and recover which already includes the stack trace for you. The functionality is already there exactly as you desire when you do need to bend Go beyond what it is intended for.
That simple example in Python is missing all the other stuff you have to put around it. Go would have another error check, but I get to decide, at that point in the execution, how I want to handle it in this context
... with no other context whatsoever, so you can't glean any information about the call stack that led to the exception.
Exceptions are really a whole different kettle of fish (and in my opinion are just strictly worse than even the worst errors-as-values implementations).
If you want to add 'proper' error types, wrapping them is just as difficult in Go and Rust (needing to implement `Error` in Go or `std::Error` in Rust). And, while we can argue about macro magic all day, the `thiserror` crate makes said boilerplate a non-issue and allows you to properly propagate strongly-typed errors with context when needed (and if you're not writing library code to be consumed by others, `anyhow` helps a lot too).
In practice, this ends up with several issues (and I'm just as guilty of doing a bunch of them when I'm writing code not intended for public consumption, to be completely fair).
fmt.Errorf is stupid easy to use. There's a lot of Go code out there that just doesn't use anything else, and we really want to make sure we wrap errors to provide 'context' since there's no backtraces in errors (and nobody wants to force consuming code to pay that runtime cost for every error, given there's no standard way to indicate you want it).
errors.New can be used to create very basic errors, but since it gives you a single instance of a struct implementing `error` there's not a lot you can do with it.
The signature of a function only indicates that it returns `error`, we have to rely on the docs to tell users what specific errors they should expect. Now, to be fair, this is an issue for languages that use exception's - checked exceptions in Java notwithstanding.
Adding a new error type that should be handled means that consumers need to pay attention to the API docs and/or changelog. The compiler, linters, etc don't do anything to help you.
All of this culminates to an infuriating, inconsistent experience with error handling.
The proof is in the pudding, though. In my experience, working across Go codebases in open source and in multiple closed-source organizations, errors are nearly universally wrapped and handled appropriately. The same is not true of Rust, where in my experience ? (and indeed even unwrap) reign supreme.
I have to say that's the first time I've heard someone say Rust doesn't have enough return types. Idiomatically, possible error conditions would be wrapped in a Result. `foo()?` is fantastic for the cases where you can't do anything about it, like you're trying to deserialize the user's passed-in config file and it's not valid JSON. What are you going to do there that's better than panicking? Or if you're starting up and can't connect to the configured database URL, there's probably not anything you can do beyond bombing out with a traceback... like `?` or `.unwrap()` does.
For everything else, there're the standard `if foo.is_ok()` or matching on `Ok(value)` idioms, when you want to catch the error and retry, or alert the user, or whatever.
But ? and .unwrap() are wonderful when you know that the thing could possibly fail, and it's out of your hands, so why wrap it in a bunch of boilerplate error handling code that doesn't tell the user much more than a traceback would?
`?` (i.e. the try operator) and `.unwrap()` do not do the same thing.
It’s odd that the .unwrap() hack caused a huge outage at Cloudflare, and my first reaction was “that couldn’t happen in Go haha” but… it definitely could, because you can just ignore returned values.
But for some reason most people don’t. It’s like the syntax conveys its intent clearly: Handle your damn errors.
And maybe not quite as standard, but thiserror if you don’t want a stringly-typed error?
As for the example you gave:
File::create("foo.txt")?;
If one added context, it would be File::create("foo.txt").context("failed to create file")?;
This is using eyre or anyhow (common choices for adding free-form context).If rolling your own error type, then
File::create("foo.txt").map_err(|e| format!("failed to create file: {e}"))?;
would match the Go code behavior. This would not be preferred though, as using eyre or anyhow or other error context libraries build convenient error context backtraces without needing to format things oneself. Here's what the example I gave above prints if the file is a directory: Error:
0: failed to create file
1: Is a directory (os error 21)
Location:
src/main.rs:7 let mut file = File::create("foo.txt").context("failed to create file")?;
Of all the things I find hard to understand in Rust, this isn't one of them.If you reject the concept of a 'return on error-variant else unwrap' operator, that's fine, I guess. But I don't think most people get especially hung up on that.
I don't understand this line of thought at all. "You have to learn the language's syntax to understand it!"...and so what? All programming language syntax needs to be learned to be understood. I for one was certainly not born with C-style syntax rattling around in my brain.
To me, a lot of the discussion about learning/using Rust has always sounded like the consternation of some monolingual English speakers when trying to learn other languages, right down to the "what is this hideous sorcery mark that I have to use to express myself correctly" complaints about things like diacritics.
If I return Result<T, E> from a function in Rust I have to provide an exhaustive match of all the cases, unless I use `.unwrap()` to get the success value (or panic), or use the `?` operator to return the error value (possibly converting it with an implementation of `std::From`).
No more verbose than Go, from the consumer side. Though, a big difference is that match/if/etc are expressions and I can assign results from them, so it would look more like
let a = match do_thing(&foo) {
Ok(res) => res,
Err(e) => return e
}
instead of: a, err := do_thing(foo)
if err != nil {
return err // (or wrap it with fmt.Errorf and continue the madness
// of stringly-typed errors, unless you want to write custom
// Error types which now is more verbose and less safe than Rust).
}
I use Go on a regular basis, error handling works, but quite frankly it's one of the weakest parts of the language. Would I say I appreciate the more explicit handling from both it and Rust? Sure, unchecked exceptions and constant stack unwinding to report recoverable errors wasn't a good idea. But you're not going to have me singing Go's praise when others have done it better.Do not get me started on actually handling errors in Go, either. errors.As() is a terrible API to work around the lack of pattern matching in Go, and the extra local variables you need to declare to use it just add line noise.
But in go you can just _err and never touch it.
Also while not part of std::Result you can use things like anyhow or error_context to add context before returning if theres an error.
You can do that in Rust too. This code doesn't warn:
let _ = File::create("foo.txt");
(though if you want code that uses the File struct returned from the happy path of File::create, you can't do that without writing code that deals somehow with the possibility of the create() call failing, whether it is a panic, propagating the error upwards, or actual error handling code. Still, if you're just calling create() for side effects, ignoring the error is this easy.)- https://github.com/kubernetes/kubernetes/pull/132799/files
- https://github.com/kubernetes/kubernetes/pull/80700/files
- https://github.com/kubernetes/kubernetes/pull/27793/files
- https://github.com/kubernetes/kubernetes/pull/110879/files
- https://github.com/moby/moby/pull/10321/files
- https://github.com/cockroachdb/cockroach/pull/74743/files
Do we have linters that catch these?
Again: I think Rust as a language gets this right, better than Go does, but if I had to rank, it'd be (1) Rust explicit enum/match style, (2) Go's explicit noisy returns, (3) Rust terse error propagation style.
Basically, I think Rust idiom has been somewhat victimized by a culture of error golfing (and its attendant error handling crates).
I think the problem is Rust does a great job at providing the basic mechanics of errors, but then stops a bit short.
First, I didn't realize until relatively recently that any `String` can be coerced easily into a `Box<dyn Error + Send + Sync>` (which should have a type alias in stdlib lol) using `?`, so if all you need is strings for your users, it is pretty simple to adorn or replace any error with a string before returning.
Second, Rust's incomplete error handling is why I made my crate, `uni_error`, so you can essentially take any Result/Error/Option and just add string context and be done with it. I believe `anyhow` can mostly do the same.
I do sorta like Go's error wrapping, but I think with either anyhow or my crate you are quickly back in a better situation as you gain compile time parameter checking in your error messages.
I agree Rust has over complicated error handling and I don't think `thiserror` and `anyhow` with their libraries vs applications distinction makes a lot of sense. I find my programs (typically API servers) need the the equivalent of `anyhow` + `thiserror` (hence why I wrote `uni_error` - still new and experimental, and evolving).
An example of error handling with `uni_error`:
use uni_error::*;
fn do_something() -> SimpleResult<Vec<u8>> {
std::fs::read("/tmp/nonexist")
.context("Oops... I wanted this to work!")
}
fn main() {
println!("{}", do_something().unwrap_err());
}
Ref: https://crates.io/crates/uni_errorWhich is why it's weird to me that the error handling culture of Rust seems to steer so directly towards where Go tries to get to!
I have a love/hate relationship with Go. I like that it lets me code ideas very fast, but my resulting product just feels brittle. In Rust I feel like my code is rock solid (with the exception of logic, which needs as much testing as any other lang) often without even testing, just by the comfort I get from lack of nil, pattern matching, etc.
The joke I like to snark about in these kinds of comparisons is that I actually like computer science, and I like to be able to lay out a tree structure when it makes sense to do so, without consulting a very large book premised on how hard it is to write a doubly-linked list in Rust. The fun thing is landing that snark and seeing people respond "well, you shouldn't be freelancing your own mutable tree structures, it should be hard to work with trees", from people who apparently have no conception of a tree walk other than as a keyed lookup table implementation.
But, like, there are compensating niceties to writing things like compilers in Rust! Enums and match are really nice there too. Not so nice that I'd give up automated memory management to get them. But nice!
I'm an ex-C++/C programmer (I dropped out of C++ around the time Alexandrescu style was coming into vogue), if my background helps any.
It doesn't? In Go, I allocate (new/make or implicit), never free. In Rust, I allocate (Box/Arc/Rc/String), never free. I'm not sure I see the difference.
> Sometimes being able to make those decisions is useful, but usually it is not.
Rust makes you think about ownership. I generally like the "feeling" this gives me, but I will agree it is often not necessary and "just works" in GC langs.
> I actually like computer science, and I like to be able to lay out a tree structure when it makes sense to do so, without consulting a very large book premised on how hard it is to write a doubly-linked list in Rust. The fun thing is landing that snark and seeing people respond "well, you shouldn't be freelancing your own mutable tree structures, it should be hard to work with trees", from people who apparently have no conception of a tree walk other than as a keyed lookup table implementation.
I LOVE computer science. I do trees quite often, and they aren't difficult to do in Rust, even doubly linked, but you just have to use indirection. I don't get why everyone thinks they need to do them with pointers, you don't.
enum Node {
Leaf,
Branch {child: Box<Node>, parent: Option<Box<Node>> },
}
Compared to something like Java/C# or anything with a bump allocator this would actually be slower, as Rust uses malloc/free, but Go suffers from the same achilles heel here (see any tree benchmark). In Rust, I might reach for Bumpalo to build the tree in a single allocation (an arena crate), but only if I needed that last ounce of speed. do {
let file = try FileManager.create(…)
} catch {
logger.error("Failed creating file", metadata: ["error": "\(error)"])
}
Note the try is not actual CPU exceptions, but mostly syntax sugar.You can opt-out of the error handling, but it’s frowned upon, and explicit:
let file = try? FileManager.create(…)
or let file = try! FileManager.create(…)
The former returning an optional file if there is an error, and the latter crashing in case of an error.Rust used to not have operator?, and then A LOT of complaints have been "we don't care, just let us pass errors up quickly"
"good luck debugging" just as easily happens simply by "if err!=nil return nil,err" boilerplate that's everywhere in Golang - but now it's annoying and takes up viewspace
This isn't true in my experience. Most Go codebases I've worked in wrap their errors.
If you don't believe me, go and take a look at some open-source Go projects.
In Rust I could have done (assuming `anyhow::Error` or `Box<dyn Error + Send + Sync>` return types, which are very typical):
let mut file = File::create("foo.txt")
.map_err(|e| format!("failed to create file: {e}")?;
Rust having the subtle benefit here of guaranteeing at compile type that the parameter to the string is not omitted.In Go I could have done (and is just as typical to do):
f, err := os.Create("filename.txt")
if err != nil {
return err
}
So Go no more forces you to do that than Rust does, and both can do the same thing. ?
is too strong.The UX is terrible — the path of least resistance is that of laziness. You should be forced to provide an error message, i.e.
?("failed to create file: {e}")
should be the only valid form.In Go, for one reason or another, it's standard to provide error context; it's not typical at all to just return a bare `err` — it's frowned upon and unidiomatic.
You could have done that in Go but you wouldn't, because the allure of just typing two words
return err
is too strong.Quite literally the same thing and the only difference is bias and habit.
You are also not forced to add context. Hell, you can easily leave errors unhandled, without compiler errors nor warnings, which even linters won't pick up, due to the asinine variable syntax rules.
This really isn't an issue in practice. The only case where an error wouldn't uniquely identify its call stack is if you were to use the exact same context string within the same function (and also your callees did the same). I've never encountered such a case.
> You are also not forced to add context
Yes, but in my experience Go devs do. Probably because they're having to go to the effort of typing `if err != nil` anyway, and frankly Go code with bare:
if err != nil {
return err
}
sticks out like a sore thumb to any experienced Go dev.> which even linters won't pick up, due to asinine variable syntax rules.
I have never encountered a case where errcheck failed to detect an unhandled error, but I'd be curious to hear an example.
err1 := foo()
err2 := bar()
if err1 != nil || err2 != nil {
return err1 // if only err2 failed, returns nil!
}
```
func process() error {
err := foo()
if err != nil {
return err
} if something {
result, err := bar() // new err shadows outer err
if err != nil {
return err
}
use(result)
}
if somethingElse {
err := baz() // another shadow
log.Println(err)
}
return err // returns foo's err (nil), baz's error lost
}
``` if somethingElse {
err := baz()
log.Println(err)
}
Good luck!As for your first example,
// if only err2 failed, returns nil!
Yes, that's an accurate description of what the code you wrote does. Like, what? Whatever point you're trying to make still hinges on somebody writing code like that, and nobody who writes Go would.Now, can this result in bugs in real life? Sure, and it has. Is it a big deal to get a bug once in a blue moon due to this? No, not really.
It's quite ridiculous that you're claiming errors can be easily left unhandled while referring to what, a single unfortunate pattern of code that will only realistically happen due to copy-pasting and gets you code that looks obviously wrong? Sigh.
"Easily" doesn't mean "it happens all the time" in this context (e.g. PHP, at least in the olden days).
"Easily" here means that WHEN it happens, it is not usually obvious. That is my experience as a daily go user. It's not the result of copy-pasting, it's just the result of editing code. Real-life code is not a beautiful succession of `op1, op2, op3...`. You have conditions in between, you have for loops that you don't want to exit in some cases (but aggregate errors), you have times where handling an error means not returning it but doing something else, you have retries...
I don't use rust at work, but enough in hobby/OSS work to say that when an error is not handled, it sticks out much more. To get back on topic of succinctness: you can obviously swallow errors in rust, but then you need to be juggling error vars, so this immediately catches the eye. In go, you are juggling error vars all the time, so you need to sift through the whole thing every goddamn time.
Go's wrapping of errors is just a crappy exception stack trace with less information.
> It seems the Go development team has a high bar for adding features to the language. The end result is a language that forces you to write a lot of boilerplate code to implement logic that could be more succinctly expressed in another language.
Being able to implement logic more succinctly is not always a good thing. Take error handling syntactic sugar for example. Consider these two snippets:
and: The first code is more succinct, but worse: there is no context added to the error (good luck debugging!).Sometimes, being forced to write code in a verbose manner makes your code better.