> Proponents of exceptions may say, “this is so much manual writing labor, exception stack trace would automate that!”
Yes indeed.
> by writing custom messages, we can provide more details useful for debugging (for example, the os.ReadFile helped us with a filename);
Custom exceptions anyone ?
> by not relying on code line numbers, the messages are more long-lived, and can actually be understood and reasoned about in isolation, without access to the source code.
Doubt...
I quite like Go (some aspects of it anyway) and it's proven both useful and successful, but some people will go great length to find virtues in its weaknesses.
This is a great post. I also respect Go. However, I don't understand the allergy to exceptions. Even the part about them adding extra debug details to the message. Hmm, looks just like a chained exception from Java/Python. Oh wait, without the helpful exception stack trace.
It reminds me of the multi-year arguments about generics in Go. Obviously, they were needed, yet 100s(?) of blog posts were written about why we didn't need them. They have aged like fresh milk -- not well. See the same discussion about the keyword "final" in C++ to seal a class. For years, C++ was allergic to ideas from any other language that came after. Things are different now.
I think the main problem with stack traces is that they incur huge performance hit whenever they are created. Wrapping errors with each error having their own stack trace can create huge performance problems if they happen as normal course of the application.
Panics in go do have traces, in practice they work like RuntimeExceptions in Java.
Overall I think Go makes a good compromise in this aspect, but it requires the developer to know how to structure their error handling in a way that it is easy to debug when the errors do happen (which is also true, and potentially harder, with exceptions).
This is why I tend to think 'catch' blocks would ideally run in the dynamic scope of the 'throw' such that you can decide whether or not to gather a stack trace at the point of catch rather than the point of throw.
Common Lisp conditions are the only thing I've used that provides that.
(well, perl's $SIG{__DIE__} + local to dynamically scope the override will somewhat let you do that, but it's a bit deep magic and something developers normally only ever interact with through a library rather than a basic feature)
I think the reality re generics was that the authors of Go wanted to see what people did instead to understand the patterns of usage so when they -did- add generics they could provide the simplest thing required.
Which is probably the only real way to figure out what 'simplest' is, I deliberately spent a couple of years writing OO perl code raw (i.e. the low level bless() based stuff that you normally only use for optimisations) to figure out which features of p3rl.org/Moose I couldn't live without - and came out the other side with a solid spec for p3rl.org/Moo which has since proven pretty much spot on as "what almost everybody needs almost all of the time."
Using their entire userbase as guinea pigs rather than only themselves is only going to work better for that.
(I'm not necessarily endorsing the choices, but I can see why they were made and why they were effective)
You don't need the horrible mess of exceptions to get stacktraces on errors.
You can tack on stacktraces on go's errors manually, but it's somewhat limited. Rust has some env var to enable stacktraces IIRC, which is a better solution.
Golang error handling is terrible by default. The lack of sum types also make it hard to know what errors a function might return and libraries rarely wrap their errors. There also isn’t any stack trace by default which makes it hard to find the original source of the error.
This is good advice. I had a hard time with this aspect of go until wrapping clicked. I think (as mentioned in the post) creating custom errors can be extremely useful as well, in moderation. Go error handling is really not that bad if you embrace it and use it well.
Like many things in go, I think a lot of us unwittingly hope it will behave in familiar ways because so much of the language seems simple or intuitive. Unfortunately, without a good grasp on it, many parts of go will appear to cooperate with your hopes until it doesn’t.
I think a great example of this is the type system. There is a correct way to use it, but it’ll let you misuse it quite extensively before things start to implode. Errors are a bit like this. You can return raw errors for a long time and it’ll seem fine until suddenly, debugging and tracing issues is incredibly inconvenient and unintuitive.
I found the book “Learning Go” really helpful in breaking these bad habits and helping me get acquainted with how things actually work under the hood.
Oh, whoops. That’s the right one. I should have specified! I found it a perfect level to get a competent programmer familiar with Go without oversimplifying or omitting too much. I came away from it significantly more productive and confident with the language.
Given how pointedly opinionated Go is about how codebases should look like (unused variables are a compiler error!?), I'm somewhat surprised that `if err != nil return err` isn't some kind of built in warning. I do think Go as a language encourages this bad behavior.
Go is not really opinionated about unused variables, it's opinionated about not having compiler warnings, only errors. Which was a mistake, given that external linting tools had to pick that up.
Go is absolutely opinionated about unused variables, and its opinion is "that's a compiler error." It's part of what makes Go special. You should try writing some sometime.
> "Unused vars are warnings" is not a possible opinion for go, period. It's also not a possible opinion for go tooling like 3rd party linters.
Right, but that doesn't mean that Go doesn't have an opinion about unused variables. Go's opinion is that they are compiler errors.
> my point is that it's not a design choice but a side effect of choosing "we don't have compiler warnings".
What makes you think that? I don't think "we don't have compiler warnings" was behind the choice to make unused variables a compiler error. It seems like a deliberate design decision on the part of the Go team.
> Some have asked for a compiler option to turn those checks off or at least reduce them to warnings. Such an option has not been added, though, because compiler options should not affect the semantics of the language and because the Go compiler does not report warnings, only errors that prevent compilation.
> There are two reasons for having no warnings. First, if it's worth complaining about, it's worth fixing in the code. (Conversely, if it's not worth fixing, it's not worth mentioning.) Second, having the compiler generate warnings encourages the implementation to warn about weak cases that can make compilation noisy, masking real errors that should be fixed.
So I comment out a line or move around some code and want to quickly run it, or run the tests, whatever. This results in a cascade of either removing more and more lines and imports, or not only renaming to _ but also changing := to =.
This is of course the usual golang gaslighting. "It was easier to implement" becomes a "deliberate design decision".
There are tons of things that linters warn about (today), that should have been errors too by their logic.
Real question: For other languages, like C and C++, that warn about unused variables, do you ignore these warnings? When would it be a good idea to allow unused variables?
There's plenty of stuff that I have checked by a lint test before commit but is allowed to stick around in the codebase for a while locally as I work.
e.g. if I have a debugging function that I'm adding/removing calls to a lot I'd like to be able to leave the import for it in place until I'm done even if my current step isn't using it.
As a TS dev, I found that this type of checking will blow up a codebase to the point where it becomes significantly harder to read. Worse, I've seen a good developer spend way too much thought and code on correctly reporting cases that realistically won't even ever happen. Since TS has stack traces, I find it much more practical to handle based on broader categories: Errors for users need some system for displaying messages, icons etc.. Errors for admins need similar handling, but it makes little sense to spend this much effort on pretty error handling when the error is for a developer.
That's the key here - I would not need anything like this in Python either, because it has stack traces.
But Go designers decided to really drop the ball here - there are no stack traces on "err" values, so one has to implement them by hand as article demonstrates.
Which is a pity. Go has a lot of great things going for it, but the error handling just makes everything unnecessary verbose.
For small codebases it might work to just grep and hope to find unique error messages. But it feels very brittle. Go should have an option to turn on stacktraces for all errors.
We wrap errors with stacktraces at library boundaries so that we can `return err` in most cases, unless we really want to add some information. But mostly we use sentry to push log messages with better data into breadcrumbs leading to the errors.
What I see wrong with Go's error handling is that the error type is an open set which welcomes you with a Pandora's box full of infinite possible error values.
This is much too burdensome for me to cognitively bear so I prefer a closed set model as seen in Zig, Rust, or any ML.
I don't recall if Swift has exhaustive error handling.
If done comprehensively, this strategy has the potential to be much better than a traditional stacktrace because you can embed local variables within each additional error context string.
The main problem with traditional stacktraces is that, while you get line numbers, local variables at every stack frame are not preserved. So unless the error is obvious like FileNotFound, you’re left guessing as to how the error happened.
While still being hella verbose and probably too much effort for most code, the upfront investment would obviate the need for adding debug print statements or dropping into a breakpoint to inspect the code after an exception.
That said, the post does feel like copium for the ridiculous verbosity in Go and re-inventing chained exceptions.
If you have any architectural complexity in your program, good error handling often come with custom errors. Unique error strings just don't cut it, you need a better structured type than a string to apply any kind of logic.
Still not seeing how this saves you from having to check if err != nil everywhere.
Though I suppose if this were another language it would be akin to a try catch around every line of code. Which in a way is great, but pretty labor intensive.
Because Rust got it right. Zig is THE language I’m looking forward to growing which adopts a lot of the best parts of Rust while keeping things simple like Go.
Can you provide some concrete examples of where the Rust standard lib is superior to the Zig standard lib? I'm talking about the library, not the language.
But I'll add that I'm happier in languages that are less structured with types and things. Perl, PHP, Erlang. Where I can just write my code and see if it works, and add checks later, if need be. Erlang's model of 'let it crash' is perfect for me, make the happy path with error checking, but only add error handling if you need to; with hotloading in production so there's little disruption. Faffing about with casting u64 to and from usize when everyone knows I'm only ever running on 64-bit is madness. C feels easier to write than Rust, but then that's because C doesn't care if you tell it to do something stupid, it'll just do it.
Also, I don't generally use an IDE, because I'm happy with an editor in one shell and another to compile and run. I do use an IDE when making terrible Android apps and terrible Arduino kinds of things; they're useful, but I can't focus in an IDE the way I can in a terminal text editor, and I've got 25 years of experience in the text editor, so I'm not planning to change.
Yes indeed.
> by writing custom messages, we can provide more details useful for debugging (for example, the os.ReadFile helped us with a filename);
Custom exceptions anyone ?
> by not relying on code line numbers, the messages are more long-lived, and can actually be understood and reasoned about in isolation, without access to the source code.
Doubt...
I quite like Go (some aspects of it anyway) and it's proven both useful and successful, but some people will go great length to find virtues in its weaknesses.