Go's Error Handling Is Perfect
blog.verygoodsoftwarenotvirus.ru
blog.verygoodsoftwarenotvirus.ru
There's no error handling in the python code, yet the stack trace will lead you to exactly to where you need to look. To me that is strictly superior when it comes time to debug an error.
Edit: maybe not strictly superior, as has been mentioned in replies, wrapped errors with good context at each step does seem like it would be very high SNR.
I do not care about hiding my debug symbols. Anything I write that has a binary you can touch, you can just go read the source directly.
go run main.go
Using the go debugger in your IDE will blow up when that part of the code that panics on error is hit. Do you still need a stack trace then? You're literally looking at it in your editor. There's a time and a place for stack traces in my view and I enjoy the fact that in Go, it's not on by default.If you're building a cloud service in Go and you're unsure of your branching paths, by all means, use stack traces and eliminate your own errors.
I agree that wrapping errors that way is better than not, but I still find it a bit lacking.
Go, and it's error handling (and for that matter it ludicrously verbose failure to bubble up errors) is a steaming pile of shit.
This is domain dependent, but most of the time errors are handled by “catch it, log it, display an oops and move on”, and optimizing for that case is very pragmatic.
I know typed errors in Java are regarded by many as a failure... but it is still conceptually very appealing to me.
I love result types and pattern matching in more typed/functional languages, except that they often have similar issues to go re: not having a stack trace.
Most logger in Go can it though.
Its funny that were having this conversation because "discount service" is a thing in my life. Its sporadic failures means I just re-try immediately and get a success... Errors are part of flow control.
What makes it not a good practice?
> you don't need 20 lines of stacktraces
I don't _need_ stacktraces, but they are useful to me.
So I disagree. This is a pedantically narrow view of what "error handling" is. Printing a stack trace and crashing is also "handling" it.
Not really, no? To first approximation all errors are system call failures: file not found, OS couldn't hand you enough memory so you got a heap allocation failure, network timeout, etc... You know exactly where those can happen because they are exposed as functions with some kind of error code specified as part of the return value.
There are situations where that's not true, but those situations tend to be fairly rare: things like "schema" violations in some backend data store ("product has a null manufacturer field", which should never happen but it turns out it does for data imported via that merger 2 years ago). And these tend to have correspondingly complicated handling ("add it to the queue of manual fixup requests") not well captured by "print a stack trace and crash".
How does this help predict whether they are handled or not higher up the stack?
And you're absolutely right, if you don't handle errors than a stack trace is a great way to reverse engineer the context. But that's not "handling" the error, it's debugging. And those are different tasks.
in fact i print file and line with every log message.
https://github.com/nathants/libaws/blob/87fb45b4cae20abd1bb1...
I like this better, because you have the option of very easily doing the "Go style" of checking for success or failure if you want, but you can also exploit the fact that it's a monad and a functor and thus you can just use map and/or bind for all the downstream functions, so you don't have to explicitly check if you don't want to.
I feel like this is the best of both world; it still forces you to be explicit (as it's compile-time enforced by the type system), but it gives you nice syntax to make the code cleaner.
Is that falling out of style? Sounds like Rust's Result type, but i'm unfamiliar with monads/etc.
If it is similar, i spent ~5 years in Go and have now had about equal time in Rust - i vastly prefer those algebraic types for errors. Notably because it _can_ be as simple as Go's string concat behavior, but it can also be far more expressive. Yes, Go's type can also be more expressive by adding interfaces/etc, but it's difficult to beat a good enum in my experience.
At risk of sounding Rust biased, proper Enums are something that would have made Go more elegant imo. Unlike Generics/etc which are a contentious topic (or used to be, at least), proper enums are just so damn handy and cost so little. My 2c.
I'm a bit outta the loop in the pure FP world now, because I've been stuck doing Java and C# for the last 5 or so years, but I think that world is trying to move to an algebraic effects-style system. People keep trying to explain to me why that's "better" but I'm not really sold on it, because it just feels like we're reinventing goto, though generally when I have contrary opinions like that it turns out that I'm wrong.
I don't really know Rust very well, but I suspect it has a similar way of doing `map` and `bind` to handle downstream stuff, since it has functional roots. The biggest issue I ever had with it in F# is the fact that they don't really have a concept of a monad transformer and therefore you can't easily combine monads, so if you have to mix `async` and the `Choice` monads (more or less F#'s version of Either), you have to explicitly craft your own custom monad. It's really not that bad but it's less sexy than proper monad transformers.
Algebraic data types are so utterly useful that I get annoyed that it's not standard in every language.
Rust's `Result` type does indeed have a `.map()` function, which works how you'd expect (and `Result`is an iterator, which give syntax for it).
Yeah, algebraic data types are very nice, and feel like such a free lunch. I cry every time i have to implement something that should have been a one-line sumtype definition as a class hierarchy.
The F# For Fun and Profit blog has a decent post explaining it. That blog doesn't appear to be maintained anymore, but it is archived: https://web.archive.org/web/20180329001511/https://fsharpfor...
You can find examples in Scala with `for` comprehensions as well.
Optional<String> result = returnsAnOptional();
if (result.isPresent()) {
log.info("result is {}", result.get());
}
// and
returnsAnOptional().map(r -> log.info("result is {}", r);I don't know that there's a clean way to do the Either monad in Java without algebraic data types. You could certainly make a wrapper class that maybe has an optional `Left` and `Right` private variable and then make a `map` that handles the unboxing of those, but I think that might be a bit messy.
Don't worry, I won't deploy it until my team has seen it ;)
Or, more likely, you just want to pass that error back up to your calling function to decide what to do with it, so you just put a ? on the end of your function call like this:
let result = potentially_failing_function()?; let res = riskFunc "input"
match res with
| Choice1Of2 result -> // successfulPath
| Choice2Of2 err -> // Error Handling!!!
That's more or less what I meant by "not radically different". The implementation is of course extremely different, but I feel like from an end-user perspective it's superficially pretty similar.There's an overhead associated with boxing and unboxing, so you might be paying a small performance penalty, but since most of what I was doing involved talking to a network a lot, that overhead was immediately dwarfed by IO latency anyway. I don't have any numbers on this, but I suspect that a very large percentage of "risky" functions involve IO of some kind anyway, so the overhead associated with boxing and unboxing is kind of negligible.
The lack of algebraic data types and nice mapping constructs is the main reason I almost never end up using Go for any personal projects. I still kind of like the language, I think CSP is a pretty decent concurrency abstraction, but it always feels like it's fighting me.
I never encountered this problem with Python, not even in a project with more than 1 million lines of code. Are there any real examples where a Python stack trace is truncated in such a way?
It almost sounds like his whole reasoning is based on the assumption that he won't be able to understand an error in Python when looking at a stack trace.
in his python example, it shows exactly where the error is (down to pointing the function), and the error is quite explicit.
> Many levels of try/except nesting could
At least to me, this is an anti-pattern in any language that supports try-catch type error handling.Many levels of try-catch should almost always be eliminated except for a small handful of use cases.
Unless you 1) can recover, 2) can retry, 3) need to log some local context, 4) need to transform the lower level error (including transforming it for display purposes), my heuristic is that it should always be handled at the highest level possible in the stack and just let the exception pass through.
But generally, for the user, I posit that it should still be handled at the highest level of granularity and the message returned to the user on an error should be similarly high level.
I try to use exception chaining to create a message that is useful for the user to actually debug the problem (solution-directed error message).
The classic toy case is either getting back "permission denied" or "can not open foo" but not both. Chaining of error messages gives a straightforward mechanism for this.
Then, the high-level text, along with the caused-by messages, is displayed without a stack trace to hopefully give the user enough information to address an issue.
Chaining can be done with explicitly returned errors as well as exceptions. The hard part is actually thinking about "is this use to the end user"
But when the rubber meets the road and you are trying to write code that consistently works, Go’s verbose but simple error system is really an asset. I used to write a lot more Go and have been writing Python, and I miss Go’s style terribly. Errors as values, the build/package tools, gofmt, and the consistent quality of the standard library are what I miss most.
Now, for the record, I happen to love Go and agree with the author about its error handling paradigms. That said, I like the python script at the top of the article a lot more still, because the problem being presented is a routine kind of thing that needs to be solved on a routine, ad-hoc basis a zillion times every day. And we don't want people writing the 80 line Go code to do that, we want them doing it in python.
Basically exception-style "I don't know what went wrong, just that it failed, let the user sort it out[1]" code is the correct solution to probably 90% of workaday problems we face. You fall through to precise error handling (and/or frameworks like in Rust that force the handling syntactically) only when you need to write code that survives in the absence of a traditional "operator". But most code doesn't run that way!
[1] In this case, the problems are likely bad filenames, bad URL, server down, etc... If your developers are running this script themselves, they'll figure that out on their own.
You don’t get anything similar in exception-based systems.
Of course, algebraic error types share this advantage. They are good too.
if err != nil {
return nil, err
if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
return nil, err
if err != nil {
return err
if _, err = reader.Read(); err != nil && err.Error() != "EOF" {
return err
if err != nil {
if err.Error() == io.EOF {
break
}
return err
if err != nil {
fmt.Printf("Failed to fetch data from %s: %v\n", url, err)
continue
if err := processCSV(csvFile); err != nil {
fmt.Printf("Error processing CSV: %v\n", err)
Everything just bubbles-up. You've reinvented Exceptions with more boilerplate. Nothing here is 'handled' beyond that.Although I don't read Go, it looks as if main() returns success to its caller even in the failure case.
So the Python code "which doesn't handle exceptions" exits in failure properly with a good error message and stack trace. The Go code "which handles all aforementioned errors" returns success for a failed operation, printing a less clear error message than the Python.
It's a singular line change to return proper OS exit code on error in the main function. And what error message is clearer is not an objective fact but only a matter of preference.
I'm getting hung up on what the author chose to publish.
The author asked,
>> do you want to know when something bad can happen in your program, or don’t you?
and then published some Python code and some Go code. Of the Go code he wrote:
>> Wouldn’t you know it, we have all the aforementioned errors handled!"
It's not some subtle gotcha, it's the point of the article and he somehow managed to demonstrate the opposite.
It would be like switching from garbage collection to C to make sure you remember to free all your mallocs, then showing demo code that's only a singular line change away from returning all its memory.
And readability. It's quite subjective and I can work with both the Python add Golang errors.
I'll can only take it as it's presented, in good faith. The author chose what to publish and compare. If I've been trolled, so be it.
> These errors should be wrapped to add the specific context that the caller might want in order to handle the error.
That would be an Exception and its stack trace.
Being able to use `?` in a method that returns `Result<T, Err>` is so elegant and helps readability compared to multiple if err != nil statements in go. In a larger program, ? lets you pass along errors up along the chain until you get to a place where they actually need to be handled, and this lets you consistently deal with them as well. For example, in a slightly more complex example [0], I handle errors on line 162 from monitor_device() (line 176, 181), instead of cluttering up monitor_device() unless I need to. (This was just some prototyping code so please bear with me).
[0] https://github.com/andrewzah/fst_rs/blob/master/src/main.rs#...
No actually, I want to know when consequential errors that I can be responsible for happen.
I don't really care that reading a file could fail (and it can fail in such a variety of ways, there's like 3 dozen distinct errors that could be thrown!) because I can't do anything about that. But if there are errors that I could recover from, that I would care about.
Unfortunately while it's possible, very little code distinguishes between error kinds or returns custom errors in go, so your bet is usually parsing error messages, while in Python or rust you have the ability to handle errors that are relevant to you, and let irrelevant ones percolate away.
Go on the other hand encourages doing nothing except logging, because it's somewhere between difficult/unidiomatic and impossible (depending on the libraries your building on top of) to handle only certain kinds of errors.
As a concrete example, in that csv parsing code, how do you differentiate between a file not found, file too large, and lacking permissions to read it? The stack overflow consensus is that doing that is unidiomatic, and you should just print the error to the user, which means this is just boilerplate for an exception that will ultimately gracefully end processing and print an error/stacktrace to the user. A bunch more typing for precisely the same intent as you get for free in Python or Java.
from https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-...
> The point is, this function signature makes it impossible for us to access an invalid/uninitialized/null Metadata. With a Go function, if you ignore the returned error, you still get the result - most probably a null pointer.
One of the practices you'll see better Python devs use is to wrap stuff in try-catch-except blocks, especially around consequential code.
See all the syntax sugar C# has for handling nullable types. That could easily be done for error handling as well. Or see Odin which has the exact same error handling mechanism as Go but with proper constructs to make it painless.
Error couldn't parse data: couldn't read csv : file not found
Manually appending what you're trying to do at each level that checks for errors seems... Error prone.
There is no objectively "perfect" approach. I personally prefer getting a stack trace of the point at which the problem occurred. I find that more useful as opposed to getting back an error code which has been propagated and modified through layers of function calls and is often hard to correlate with the original error condition.
I’ll bite. Idiomatic Go code, as in the article, does:
response, err := somethingorother()
In the error case, response needs to have a value, and nil serves this purpose. If response were a non-nillable type, this would not work.Result-style types solve this problem.
Way, way less extendable. You'll have to completely rewrite the Go code if you want to put it in a library.
Now I'm doubting the author discernment on building that opinion.
I would agree that complaints about go's error handling are really silly because said complaints usually come from people that work in languages with exceptions where, if they choose to handle errors, need to handle them in precisely the same way as in go, just in an even more verbose form.
The only reason people coming from these languages don't like go is because go actually forces you to either deal with the error or explicitly ignore it rather than just letting your functions propagate failures, which in theory is nice but in practice leads to horrible user experience because the buck wound up not stopping anywhere up the call stack since every caller decided "someone else can handle it".
As every programer does you sometimes need to write throw away code. The one off script, a quick tool to clean something up, a loop and a regex to make it cleaner data, aggregating data from two systems into one handy file.
Go has this handy way of throwing away an error return. If I have to write something that is trash I skip over the errors. I can ignore missing files or json formatting issues. Granted that code would not pass muster of a code review, it would be apparent what I'm doing.
You can cut corners in go, it just reminds you that your doing it.
use std::error::Error;
use csv::Reader;
use reqwest::Client;
use serde_json::Value;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Define the CSV file path
let file_path = "data.csv";
// Open the CSV reader
let mut reader = Reader::from_path(file_path)?;
// Create a new HTTP client
let client = Client::new();
// Iterate over each record in the CSV
for record in reader.records() {
let record = record?;
// Get the value from the first column
let url = record[0].to_string();
// Perform GET request
let response = client.get(&url).send().await?;
// Check for successful response
if response.status().is_success() {
// Read response body
let body = response.text().await?;
// Parse JSON
let json: Value = serde_json::from_str(&body)?;
// Process the parsed JSON (e.g., print it)
println!("JSON for URL {}: {}", url, json);
} else {
eprintln!("Error fetching URL: {}", url);
}
}
Ok(())
}
The thing that makes it perfect in Rust, that you can keep the above for a simple script, but if you want to solidify it you just think about how you want to handle the `?`-annotated spots (retries, error-repacking, panics, etc.) use csv::ReaderBuilder;
use reqwest::blocking::Client;
use serde::Deserialize;
use std::error::Error;
#[derive(Debug, Deserialize)]
struct Data {
#[serde(rename = "key")]
value: String,
}
fn fetch_data(url: &str) -> Result<Vec<Data>, Box<dyn Error>> {
let response = Client::new().get(url).send()?;
let data: Vec<Data> = response.json()?;
Ok(data)
}
fn process_csv(csv_file: &str) -> Result<(), Box<dyn Error>> {
let file = std::fs::File::open(csv_file)?;
let mut rdr = ReaderBuilder::new().from_reader(file);
// Skip the header row
rdr.headers().unwrap();
for result in rdr.records() {
let record = result?;
let url = &record[0];
match fetch_data(url) {
Ok(data) => {
println!("Data from {}: ", url);
for item in data {
println!("{}: {}", "key", item.value);
}
println!();
}
Err(e) => eprintln!("Failed to fetch data from {}: {}", url, e),
}
}
Ok(())
}
fn main() {
let csv_file = "your_csv_file.csv";
if let Err(e) = process_csv(csv_file) {
eprintln!("Error processing CSV: {}", e);
}
}
My prompt was:Please write some rust code for me that goes through every row of a CSV file, performs a get request for the value in the first column, and parses the responses as a JSON map of strings to strings.
The code should be the rust equivalent of the following go code:... I also added at the end "Use reqwest for the http calls" as chatgpt spat out some code using ureq which was worse than the reqwest version it used without telling it to make the equivalent of the go code.
The main difference I see in the two is that as a go developer the language forces you to become intentionally code-blind to the error handling ceremony. `if err != nil` is effectively punctuation, but it's treated the same as your actual program by representing it as tokens.
Go treats the tuple(result, err) as a good-enough error handling approach. Rust says here's something equivalent but better.
Almost everything can fail, and we don't need our noses constantly rubbed in it. Knowing how it failed might be interesting, but you're on your own to check for error subtypes and it's rare to define any.
2/3 of the code is way too much boilerplate for delegating error handling when it can be automated more reliably.
res, _ := some_func(…)
But it's obvious that you're doing something naughty when you do.
edit: combating the overzealous auto-correct
Go forces you to think about failure just as much as success, and I find that fantastic for building robust software.
Errors happen, so you're forced to do something when they do. Errors aren't exceptional.
But more importantly to me, Go forces you to think about what a caller to your function might want to know about what went wrong. Errors in modern Go are for communication, not an explosion of "something bad happened, abort!"
A long time ago, errors in Go were very basic -- essentially just strings 99% of the time, and I think that's where some of the hate comes from. In the early days, I think it was deserved.
But nowadays, with the Go errors package (error chaining), errors are extremely powerful.
Good libraries will often return an error structure that provides more context than a stack trace would, e.g. os.LinkError: https://pkg.go.dev/os#LinkError
tl;dr if you're writing "if err != nil { return err }", you're holding it wrong.
Except, it doesn’t.
Forcing you to at least write boilerplate code for failures might be a nudge to think about them for some people, but it absolutely is not “forcing” you to think about it, and you can absolutely defer it with boilerplate while concentrating on the success path and never actually return to it.
For example:
err := doSomething()
if err != nil {
return nil, err
}
err = doSomethingElse()
if err != nil {
return nil, err
}
// Oops unhandled
err = doSomethingElseEntirely()
// A bunch of other stuff..
return nil, nil
Would much prefer "Either" with pattern matching, or something else that actually forces you to do something.I think using a more structured capital-R Result type, combined with match/case makes for better structured code, and I can't help but think that `err != nil` is a bit of a smell left over from Go's initial decision to avoid doing generics (all of which is alluded to in the writeup).
I do disagree with the author using a trivial example to demonstrate that `err != nil` isn't that bad, since most code you'll work with isn't trivial. the errnil pattern obviously isn't enough of a problem to deter many companies, which I've always been interested in, since to me it always seemed like a brittle and awkward error handling mechanism.
All that aside, `blog.verygoodsoftwarenotvirus.ru` is an excellent domain name.
> The only opposition I have to the Result type in Go is that we wouldn’t be able to make use of it in the standard library without either breaking backwards compatibility, writing Result variants of existing API calls (so NewRequest, NewRequestWithContext, and NewRequestWithContextAndResult), or issuing new /v2 variants of existing packages (like the recently-released math/rand/v2 package), which then means we’ll have some libraries and programs that use the old style with one return value, some with the new style, and many instances of confused programmers using the wrong one. It would be as close to a Go equivalent of the Python 2/3 transition debacle as I think we could manage.
I see Go's approach and Rust's approach as being mostly isomorphic as Go's `tuple(result, err)` maps pretty reasonably onto Rusts's `either(Err(err), Ok(result))`. If Go implemented a language convention that made these 100% equivalent, there would be no real problem except the battle between the old school and new school code authors as to which one is the right one to use in their code.
Caveat - I am not a Go programmer, so I have no idea how feasible this is as an addition to the language proper. If you wanted to write Rust like you do go, it would be as simple as the following (cursed):
impl From<Result<T, E>> for (Option<T>, Option<E>) {
fn from(result: Result<T, E>) -> (Option<T>, Option<E>) {
match result {
Ok(value) => (Some(value), None),
Err(error) => (None, Some(error)),
}
}
}
then let (val, err) = someFallibleCall().into();
if val.isSome()
// something good with val.unwrap()
} else
// something bad with err.unwrap()
}
(noting of course this would be intentionally unidiomatic rust - real code would probably use match (val, err)) for this.I'd say then that it's only as perfect as you can be while being compatible with existing Go code.
My general approach is validate inputs before running "the thing".
Check if file exists
Parsing CSV file is moved to another class, do more validations on data
HTTP calls, if failed, shouldn't eject the entire algo.
FWIW, that Go code might be "safer" but it hurt my eyes.
It gets in your way at the beginning but once you get beyond a module or two, it's quite useful, novel, and under appreciated.
To me, the ideal in a language is to assume that everything will go right, to enunciate the core intention of the program, function, etc., and to handle errors somewhere else, maybe below the main code or something like that. Mixing it all in right there with the code, even if we end up concluding that this turns out to be the best way to deal with things, seems visually confusing to me when reading the code.
{:ok, result}
{:error, reason}1. Error handling is better than no error handling at all
2. Go is designed to be like Go not Rust
Yeah, your typical Go article...
I definitely agree that in many programs, when an error happens, you're done, you just blast up the stack and then the error message goes into a sentry report or gets logged etc.
But it's also still quite common to want to treat some error conditions as soft errors, or otherwise handle them differently. Now you're doing errors.Is and engaging with the poorly structured world of error wrapping trees.
Here's one I hit recently. One of my pet peeves is error messages like "unsupported thing". Ok, what thing was unsupported? Very often people just omit this stuff because it requires you to now cat strings, which is sometimes hard or cumbersome depending on the language. So, I changed my code to have the error be "unsupported thing: <thing>".
However, up the call stack, I want to handle ErrUnsupportedThing too. Because maybe there's some reasonable thing I can do to help the user make the thing supported. So I'm calling `errors.Is`. But wait, now I need an `ErrUnsupportedThing` variable to refer to. But each call to fmt.Error creates a new error that isn't `errors.Is`-equivalent to each other.
I ended up reconciling this by making `ErrUnsupportedThing` just be "unsupported thing" and then I wrap it by doing fmt.Errorf("%w: %v", ErrUnsupportedThing, thingType).
But now what if I want to say "ErrUnsupportedThing: <A sub-error that occurred>"? Well, hope you're using a recent version of go, because "%w: %w" only works in go 1.20 and later.
Also the fact that all errors are just "error" instead of being typed errors tends to make people a lot lazier about their error handling in general. In java, if I get a ThingNotSupportedException, I expect to be able to find a getUnsupportedThingType() so that I can explicitly pull it off the string without needing to parse. That's easy in Java, it's a subclass of exception. In go, now you're writing a "subclass" struct to implement error's interface, tacking on extra struct fields and stuff, and since nobody ever actually types their return values more explicitly than `error`, you're now doing casting/duck-typing to pull that information out. Etc.
I guess let me put it this way. Go errors are good for 80% of errors and fine for 95%. That's great. But the easy code is already easy! What irks me is when it breaks down when I get to the code that's actually a bit tricky to handle errors for. The type system, the error system, and everything is just like "wow good luck you're on your own". Whereas python/java/etc have my back here.