What is a Golang person... a Goist? A Goer (hehe, Monty Python). Since I am a goer, I'll use Goist here.
I'm not a Goist, but I agree with TFA in general. Don't complicate a language for error handling purposes. My reasoning is that most errors cannot be properly dealt with anyway.
Is it the Erlang or the Elixir folks to just say, paraphrased, "let it die"?
Just let it blow up. Somewhere upstream can deal with it, because chances are we don't know enough down in the trenches to know what to do when something which we expect to work virtually always, fails.
And to improve the noise on the current copy example, I would argue to stop wasting lines!!! There is no good reason to open a bracket, newline, then return err, then close the bracket. It's small and obvious.
if err != nil { return err } // put it on one line. It's clean and natural.
Edit: Oh now I remember from my limited Go experience :(. They live by their formatter, and their formatter was built in a bondage dungeon.
An exception is something unexpected, unanticipated, exceptional. It's totally fine to crash on it. Running out of memory is a good example.
An error is something expected, a less probable but anticipated result. A request timeout is a good example. It's important to have a nice way to process it, and to take an action other than crashing, say, to retry.
In this regard, `guard` is a nice proposal: it explicitly shows where we are willing to crash (like `!` in Typescript or Rust or Kotlin), while staying perfectly backwards-compatible and not requiring any rewrites. It's not perfect in other respects though; I liked the `try` proposal better.
Thanks for making this distinction. I sometimes have to stop and remind myself that. Can get crazy going up and down the stack, handling errors and exceptions in different ways.
It's also very easy in many languages to use exceptions for non-"exceptional" control-flow even though they aren't really meant for that.
I'm not sure I buy your definitions for errors and exceptions. In practice, many errors are rare cases and are essentially unexpected. They are exceptions in terms of being exceptional results compared to most results.
The biggest challenge with error -- or exception -- handling is that most callers are not prepared to deal with something other than success. So the safest thing for them to do is push the failure up, with the expectation that somewhere higher will be equipped to deal with it.
If you take TFA's CopyFile as an example, there are plenty of scenarios where copying a file would be part of some complex process. If the file copy fails, it probably indicates either a logic problem (bad path, empty filename, something) or an OS level failure. Either of those cases cannot be dealt with at layers close to the CopyFile call.
From TFA example, the guard only eliminates the if err != nil { return err }.
This is a bigger problem than can be solved with syntactic sugar. And from my perspective, it is why exceptions exist. If you merrily go on your way in some other languages and try to copy, but that fails, it will generate an exception. If you don't explicitly handle that exception in your calling function, then it will bubble up. If somewhere upstream expects this possibility, it can be designed to respond to a specific type of exception and do something reasonable about it. But your calling function, down here at a low level, will not have any clue what the business logic behavior should be if the CopyFile() fails.
> What is a Golang person... a Goist? A Goer (hehe, Monty Python). Since I am a goer, I'll use Goist here.
Gopher.
> Is it the Erlang or the Elixir folks to just say, paraphrased, "let it die"?
That sounds like panic. When the Erlang folks say that, they're talking about letting a process die. The rest of the application is in different processes and doesn't get taken down. So the offending process can be rebooted and operations can continue.
> if err != nil { return err } // put it on one line. It's clean and natural.
Oh.. erm... well that's explicitly not letting it die. That's returning it and forcing it to be manually "dealt with", as you put it. It seems like you're contradicting yourself.
Also, the article explains that returning without context is often a bad idea.
Now I know what being old means. It means you can make references to things, and younger people will completely overlook them.
Of course I can do a small bit of internet search to get the answer to my rhetorical question. But I thought (wrongly) that it would be more entertaining to make a Monty Python reference. "Is your wife a goer? Know what I mean?"
> that's explicitly not letting it die. That's returning it and forcing it to be manually "dealt with"
That is letting it die in the sense of not handling the error locally. It is allowing the failure to go upstream, and either assuming the caller will handle it, blow up, or further pass it up.
> Erlang
As I understand it, the supervisor processes ensure that the workers are up and running. If one dies, it gets restarted within the constrants of failure count limits and such.
Instead of handling errors, you can choose to allow a failure to blow up the "process" (it's not a full OS process, I believe) and be comforted in the knowledge that your process will be restarted.
If you look at many business processes (take web app activities for example), you see that failures don't get resolved in perfect tidy fashion. Instead of trying to handle the multitude of possible failure cases, you just accept that many failures cannot be recovered from. You just let it silently die, and you maybe provide a general "sorry, try again" message to the user.
That is acceptable in a lot of cases, especially when it is virtually impossible to properly handle all failure cases gracefully. Imagine if a user were uploading an image to a web app, and CopyFile() failed. How would you handle that? Would you tell the user, "Sorry, we got your upload, but we couldn't put it in its proper place, so it failed. Try again."? Or could you let it "blow up" and just tell the user, "upload failed; try again".
Now imagine you try to handle failure cases and properly report them all upstream. Now if you write tests, your tests must test for the success path and multitudes of possible failure cases and resolutions. In practice, this just is not done (outside special high risk scenarios like commercial flight and such).
I'm sorry. We're all on the internet, and I could have looked it up myself. It was really a rhetorical question as well as an opportunity to enjoy a memory of the Monty Python nudge nudge wink wink skit: https://youtu.be/4Kwh3R0YjuQ?t=25
I think it's fine to add syntactic sugar for panic. The standard library already has similar functionality, like regexp.MustCompile. And personally, I've implemented a Must function in many services. E.g. you try to parse a config file during service startup and parsing fails. There is no recourse from such an error. I'm sure I'm not the only one who has implemented such functionality, so why not add it to the language?
Regarding the guard keyword, the author's proposal doesn't make sense. The guard call comes after the function call that produces the error, so it's not clear what is being guarded. I guess it's the error being wrapped in the guard message, but explaining that clearly in plain English is quite hard. I agree that context is important however. But maybe the Go team is finally ready to admit that stack traces are more useful than error messages. I'd be fine to know that, for example, an error occurred opening a file and having the language provide a full stack trace of where the error happened. I can do without the custom crafted error messages.
Panic at compile and run time are two very different things. must will panic before you ship your code to production no should want to make panic at runtime look nice
Ah yeah i was remembering wrong. I normally have this setup as a package level variable and then build and test. But yes its on the test that it’s actually caught
I do have a lot of trouble figuring out how Go's error handling is meaningfully different in user experience here.
Errors as values seems to sound better in theory then in practice, since in practice everything is type Error, and then I either expect enough from the error to do something about it, or I don't. Which... Is exactly how I use exceptions in Python, replete with mostly having no idea what will get thrown.
But Go makes it somewhat worse because most "error values" are just formatted strings, not actual types.
But that’s the opposite of what Go did. I think they always knew generics were important, but it just took ages to come up with a design that was satisfactory. Compare to Java, whose generics had so many compromises (type erasure) that I hated them.
I really don’t think a quality gate is a problem, even if it’s sometimes inconvenient.
> And personally, I've implemented a Must function in many services. E.g. you try to parse a config file during service startup and parsing fails.
regexp.MustCompile exists to catch programmer mistakes. You've presumably done your testing and when you ship the software you believe the regexp is correct and it should be impossible to fail, so if it still fails, something exceptional has happened; an exception as we call them in the biz.
What you are doing when reading a config file is dealing with user mistakes. These are not exceptional, they are very much expected to happen and when they do you would traditionally want to provide useful feedback to the user, not simply crash.
This may be something that people do, but it isn't what you'd want them to do. panic/recover are intended for exceptions, not errors. Why make it easier for developers to do silly things? In the rare cases where you actually have exceptions to deal with, a little extra work to make the situation clear to the reader is okay.
Your distinction between user and programmer doesn’t make sense to me. I typed the regex and I typed the config file. Why is a regex syntax error different than a YAML syntax error? Both are irrecoverable errors for my programs.
The difference is that the regexp is embedded in your code as part of your code and won't change after you've shipped your code. It can only panic if your code is flawed. If the compiler was smarter, it'd be a compiler error. It only ends up a runtime concern because the compiler isn't smart enough to interpret your regular expressions to find your mistakes.
If you were loading the regexp from an alterable file at runtime, to allow the user to change it after the program is shipped, you wouldn't use regexp.MustCompile. You would use regexp.Compile and gracefully deal with any errors that the user may have made.
If you are talking about compile-time configuration, where once your program is built it won't change, why would you use YAML when you could simply use language-native variables/constants with actual compile-time safety? Regexps can improve on some programming problems over writing the equivalent code, so it's a good tradeoff in some cases. YAML is never nicer than the equivalent code.
And so failure is a user mistake, not a programmer mistake, and should be handled as such. The programmer and the user being the same person is irrelevant.
The difference is that the config is loaded at bootstrap stage - if it fails, starting service fails, there is not much to handle other than crash and supervisor (ie. k8s or whatever) will try to start it again.
The proposal the submitted article references has been inactive for years.
After submitting proposals and looking at the existing ones and thinking about this more I think the biggest problem with error verbosity is just that you have to also return zero values for the success results when all you want to do is return an error. This obfuscates the intent, making things harder to read and write. In some scenarios such as adding an additional return result it forces changes on multiple unrelated lines when it should just be a single-line change.
It's also possible that errors wouldn't be as verbose if Golang had automatic error tracing like Zig (a slightly better form of stack traces)- in that case the need for error annotations would decrease. With that and zero values removed, just allowing the error return on a single line instead of 3 might be all that is needed.
Single line reduces noise, but doesn't make it less cumbersome. My pet peeve is that when error types differ, I need to juggle between `err` and `err2`. When I refactor code and add/remove fallible functions, I need to change between `=` and `:=` accordingly only because the error has to get a named variable every time.
I agree this is awful, but isn't it solved here by not using pointers? I don't remember coming across a case where a custom error type benefited from being a pointer. I am sure such a case exists, but then such a case should be used when giving example code.
No not using pointers would not solve the issue. The issue is that the specific error implementation type is part of the method signature. If you returned type MyError instead of *MyError, then it would be impossible to return nil from the method because only pointers can be nil.
I get that. But why define the Error method on a pointed receiver? In my experience almost all errors can be immutable particularly once you start using wrapping.
Never mind- that doesn't actually make a difference.
The solution in this case is to not re-use an existing error variable- but re-use of an error variable is common practice.
Yea, the issue is that nil's in Go are typed. So `err == nil` is checking err against error's nil value, but the value stored in err is *MyError's nil value.
package main
import "fmt"
type MyError struct{}
func (m *MyError) Error() string {
return ""
}
func main() {
var err error = nil
var myErr *MyError = nil
fmt.Println(err == myErr) // this is false
}
Comparing different types normally is a type error. But for interfaces the non-interface value will be converted to the type of the interface value. This conversion gives it a boxed dynamic type. This is different type than just assigning nil, which is the untyped nil, which sets the interface to its zero value.
Normally one would not compare two interface values, so this issue really comes up when comparing an interface to nil. If Go had proper option types then this issue would go away.
var err error = nil
var myErr1 *Err1 = nil
var myErr2 *Err2 = nil
fmt.Println(err == myErr1) // this is false
fmt.Println(err == myErr2) // this is false
fmt.Println((error)(myErr1) == (error)(myErr2)) // this is false. requires the cast to do the comparison
}
Couldn't they solve this by letting you rebind/shadow `err` without requiring a new block? Then to make sure you don't accidentally ignore the error, the standard unused variables compile error would prevent that (it would right? it could at least).
Nope. This isn't even true in all situations. For a simple example, just look at description of the extremely popular io.Reader:
> An instance of this general case is that a Reader returning a non-zero number of bytes at the end of the input stream may return either err == EOF or err == nil.
> I think the biggest problem with error verbosity is just that you have to also return zero values for the success results
I agree, and this got worse with generics since T{} doesn't work, and we have to use *new(T), which is quirky.
However, I wonder if we just want an Optional/Maybe type that has support for multiple values. Without variadic/recursive generics, we'd need compiler support. But a tagged union would get rid of the zero-values.
(I somewhat agree about the io.Reader case where returning both the number of "screwed up" bytes and an error can be useful, though perhaps the number of bytes should be encoded in the error type instead. I rarely care about them.)
> (I somewhat agree about the io.Reader case where returning both the number of "screwed up" bytes and an error can be useful, though perhaps the number of bytes should be encoded in the error type instead. I rarely care about them.)
Or you can just keep it as-is because it's one of the very few cases where it does make sense to have a product type.
Well if you add sugar for errors, and as sugary as "specific to the error type as the last element of a return type, plus automatic zero values", then just make it like rust's `?` already.
I would prefer this to the status quo, but it isn't a completely clean solution to the problem. If I now make a one line change at the bottom of my function to return one additional result, the only additional change in the diff should be to the type signature. But now my diff will still show adding zero values on all the rest of the return lines. It's nice that they will only be underscores- again it is certainly better than the status quo.
I like the second snippet (The actual CopyFile function). It's verbose at the right level – from the single glance I know what will happen in all possible execution paths.
No additional cognitive load on figuring out what's going on behind the scenes on errors. Priceless.
The call to "Guardf" isn't great, and the fact you have to call it at sites that use it regardless if an error was generated or not offers the opportunity for plenty of "behind the scenes" subterfuge.
It's all an incredible amount of effort to avoid what boils down to a basic nil pointer check. I agree the blocks are unsightly, but they're as plain and flat as you could possibly hope for. I almost think you'd be better off using syntax highlighting to obscure them if they're that painful to have to look at.
Exactly. So much magic and complexity just to perform the nil check behind the scenes.
Also it has a bit of educational component. It's like a seatbelt. In some societies people consider seatbealts annoying and irritating thing that restrict their freedom. They use seat belt plugs to get rid of that annoyance. Go just forces you to use it, regardless of your initial predisposition to the seatbelts.
I like that the execution paths are visible. The issue is that the code's actual functionality gets hidden behind all the noise created by error handling.
This is how we used to write C++ twenty years ago. I used to maintain about 100,000 LOC and I never felt this was an additional cognitive load. In fact, it was beneficial to have the sequence of code always mirroring the sequence of execution (loops and conditionals notwithstanding). Syntactic sugar built into the language was never a priority (at least where I worked). Whatever abstractions we needed, we achieved through classes and operator overloading.
Honestly all I need is Java exceptions without checked exceptions nonsense. Just let me throw exception, collect entire stacktrace, let me catch exception whenever I want or dump stacktrace if nobody caught it and goroutine died. It's simple and it works. Investigate recent Java improvements with stacktrace logging (I think Spring Boot does that, basically it turns stack trace backwards as error cause often is on the last part of stacktrace) and implement as necessary.
I love golang as much as I hate its error handling. If not for it, I'd switch to it tomorrow. As it is now, I'd rather bend Java to be golang. Just because of error handling.
The only thing that I'd add to Java exceptions is to enrich the exception stacktrace with all serializable local variables on every level (including parameters). Yes, that's a lot of data and some smart approach should be taken to serialize it to string. Basically all "simple" local variables should be included to stacktrace. That'd be another level of error reporting.
Go and Rust both strike me as languages made by people who HATE Java/C++ style exception handling but thought the best way to deal with it was to invent a language that constantly punches you in the face.
the fact that the most popular Go IDE actually has logic to automatically fold and hide the constant error handling boilerplate and that people seem to have no problem with that makes me feel like I'm taking crazy pills.
I feel like Rust isn't too bad at it: if you want the almost C++/Java level of wrist strain just use anyhow::Error, remember to put ? after fallible calls and downcast to extract the underlying error.
C++ and Java exceptions have non-local flow and it's hard to determine what function can throw what, so if you do want to add error handling cases you would have to inspect every function to know if it throws or not. Yeah, there's noexcept and checked exceptions but one was a late addition that legacy codebases don't use and the other everyone just does throw new RuntimeException(err) anyway.
> C++ and Java exceptions have non-local flow and it's hard to determine what function can throw what, so if you do want to add error handling cases you would have to inspect every function to know if it throws or not
In general I agree with you, but Rust is already having this problem by people making liberal use of unwrap.
C++ exceptions and Rust Anyhow errors lack stack traces by default, whereas Java exceptions capture them by default, aiding debugging. I'm not sure if Rust spans satisfy the same requirements as stack traces; they seem more built around logging and asynchronous execution, than error handling and synchronous code.
I believe the backtrace API was added to stable rust with the most recent release, and so anyhow backtraces should now be available outside of nightly.
After several false starts, Rust mostly got this right with returning
Result<usefulresult, Error>
from most functions, and using "?" for "If the result of this is Error, do a return of the error". There's also a thing where you can write
foo().context("useful info about foo")?
and pass some more info with the error.
Plus, most things that work like "open" close when their scope closes, so you don't need all that "defer" stuff.
I would have liked a good error hierarchy like Python 2 had, where there was a clear distinction between program-is-broken errors and errors due to external causes. But Rust didn't get that at the beginning, and now it's too late.
Yes, Context is part of Anyhow. That's a legacy problem. Something like Anyhow should have been the base of almost all errors, but it came too late. If you get error handling wrong at the start of a new language, it's really hard to make changes, because the old format gets baked into everything.
I agree, except that I want checked exceptions. It’s really not different from the error part of an Either return type — and it could be implemented that way on the ABI level.
(And I’m not sure about the local variables idea, because that could become really slow and could effectively duplicate the stack in size, or even more in the case of “suppressed” exceptions).
Every modern Java library I’m aware of, avoids checked exceptions. Java itself recently introduced UncheckedIOException to wrap IOException in a few places. So while it’s not an official deprecation, it’s a sign that something’s not quite right.
Those are just objective facts. I, personally, don’t like this feature either. May be with very different language design it could be better, but as it is, it’s ergonomics is awful IMO. And I have problems to imagine how to rework it to make it work.
Well, arguably Java’s implementation is not the best regarding checked exceptions. Nonetheless I feel we threw the baby out with the bath water here, as a non-inheritance based checked exception system was not really tried and I feel it would be the best of both words. Since in the end, checked exceptions are just Result types which happens to be natively “understood” by the language - it gets stack trace support, auto-unwraps and bubbles up.
What's missing in Java is the ability for methods to abstract over the set of exceptions thrown by the methods it calls, via generics, for morethan a single exceptikn type. This would in principle be possible, similar to variadic template parameters in C++, and would solve the problem for lambdas and Streams etc. It's a bummer that Java doesn't provide this at present.
Java developers are really divided on the topic of checked exceptions, there is no consensus.
Adding or removing checked exceptions breaks library users in very painful ways: if you can’t handle an exception, you should rethrow, but this means a wavefront of code breakage downstream, so the library gets pinned and is never upgraded.
>Just let me throw exception, collect entire stacktrace, let me catch exception whenever I want or dump stacktrace if nobody caught it and goroutine died.
Go allows you to do that with panic/recover, although it's not quite idiomatic
With entire standard library that’s not just idiomatic. I would need to write wrappers or just rewrite standard library. It’s plain crazy if you ask me. So yeah, theoretically it’s possible to emulate exceptions, but in reality it’s not.
Not trying to start a flamewar, but then why don’t just use Java? It is a more concise (seriously), more expressive language with a bigger ecosystem and similar performance (go is better for smaller apps, while java is better for big server workloads). It pretty much already has virtual threads, not much will change regarding its API, so not even that is a difference.
Go's error handling is one of the worst laguage decisions ever. Exceptions are emuch cleaner and more sensible to handle. If you're worried about not handling erorrs turn on a check to force all exceptions to be handled.
Writing boilerplate error handling code for every function call is one of those things that can make you _think_ you're bring productive because you're writing a lot of code, when in reality it's mostly just tedious mechanical work of little value.
Swift does an okay job at this. Java forces you to handle _most_ but not all exceptions.
My only complaint about go error handling is that they are not encoded as unions/sum types/variants/choice types (whatever you want to call them), so in practice this leaves us the chore of checking and passing zero values and nils everywhere.
Sugar for error handling is already a solved problem (.e.g: Rust, Swift, or Zig).
Go will be damned by the community's NIH syndrome.
Sum types + pattern matching + tuples alone are so powerful, they could fix like top 5 of my biggest annoyances with Go.
Add some syntax sugar (like '?' from Rust), and it fixes a majority of the boilerplate issues.
Imo, people praise Rust too much for the ownership and safety story, and way to little for its (more simplistic) excellent decisions around simple types.
A bit of a tangent here but a similar thing happened with Python 2->3: people focus on Unicode (especially native English speakers) and completely forget that exceptions were made unbroken.
Yup. Go desperately needs a sum type with compiler-enforced matching on all possible cases. And errors need to be such a type.
Long before Rust I used to implement this (to the extent possible) with templates in C++ when I'd start work on a new code base, if it didn't exist. With generics added to Go I might start the same at my current role. It's too bad there aren't other tools to support it (e.g. macros).
> My only complaint about go error handling is that they are not encoded as unions/sum types/variants/choice types
It would not be as bad if they were typed at least. My main pain point of Go error handling is that it's all `error` everywhere, and you have to read docs or even library code to understand what kind of errors there can be and how to handle them.
I don’t mind Go’s error handling a single bit. Reducing 3 lines to 1 line is hardly a win because people will still complain that their code looks bloated. I’m not against more concise error handling I just don’t like any of the current solutions, and the status quo may still be the best.
Okay friends, the reason why Go’s errors are good as-is is because you actually have to think about them. That is a feature. If you stop thinking about them and live in happy-path land forever, you will forget that bad things can (and often do) happen during the execution of your code. Even if you just return the error up the call stack, you’re still making a decision about it. You have an opportunity to examine the error at that point and even wrap it with additional context based on what you know at that point in the call stack. The exceptional path of the code is constantly maintained instead of handed to a runtime with the hopes it will do the right thing (particularly with things like retries).
That's fair, but I don't feel that Go's position of biasing to the absolute opposite end of the spectrum is reasonable. Ultimately; in a typical app; if you've got a phrase of code like:
result, err := db.GetUser("12345")
if err != nil {
return nil, fmt.Errorf("failed to query user: %v", err)
}
There's an inherent asymmetry between how often those three conditional lines are executed relative to the undeniable fact that error handling is three times the code line volume versus the core business logic. That's not ok; and when I look at the argument phrased like that I find it hard to believe that anyone reasonable can take the stance that its desirable that error handling requires writing substantially more code than The Thing Which Generated The Error In The First Place.
I disagree, I think that handling the failure cases properly is much more complex and requires more consideration than the success cases.
Sure, in many cases you’re going to bubble up the error (perhaps a syntactic sugar for this scenario would help), but there are lots of production error handling considerations like transaction rollbacks, response statuses, logging, metrics, opentelemetry traces, retry logic, that you will need to handle in practice, and you will want to handle these differently in different failure modes.
Ensuring software acts in an expected way in the transient 0.001% scenarios is very important. With Go I never run into unhandled exceptions causing software to blow up like I do with large Python codebases.
At first I didn’t like the “if err != nil” bloat of Go code, but now I appreciate its explicitness, it is distinctly unmagical, simple, boring, and works well in production.
This isn't a situation where the choices are: 3 lines of IDE-Insert-Boilerplate, or Exceptions. I think Go could add syntax to keep its relatively unique form of error handling, while reducing code noise. The guard proposal is fine, but I'd prefer to see something like:
Syntactically really strange, but point being: if a function returns (any, ..., error); 'guard' is a keyword which eats the last 'error', optionally prefixes it with the string in that last keyword argument (standard golang error wrapping: "error Get(12345): {err.Error()}", and bubbles it up (along with zero or default values for the other arguments). It would also be nice, for more complex use-cases, if that last argument could be a mapping function as well, like:
No there's not. People seem to just want to ignore all errors, and let the language "figure it out", AKA exceptions. If you want to ignore errors, then just go all the way and do that:
result, _ := db.GetUser("12345")
then, you never have to worry about errors, you can just code in bliss and wait for the "panic" because you didn't want to bother with proper error handling.
The "_" is supposed to be used only when you're absolutely sure that error cannot happen (e.g. the function you're calling is partial, but you know that the input is in the defined set).
> [...] I find it hard to believe that anyone reasonable can take the stance that its desirable that error handling requires writing substantially more code than The Thing Which Generated The Error In The First Place.
I have personal experience with Java, Go, and C#, to varying degrees. Over the years, my personal style has changed a lot, based on my experience running applications (as someone in ops), developing applications, debugging applications, etc.
First of all, I'll say that doing Go error handling well figuring out the right place to add context to your errors. So in the example above:
return nil, fmt.Errorf("failed to query user: %v", err)
The error is from a method named "GetUser", and in the style guide I use, errors from GetUser would already say something like "get user %d: %w". You'd know this when writing code that calls GetUser, and wouldn't double-wrap it with additional context, if the additional context is not useful.
That's a bit of a tangent.
My experience working in operations is that errors in languages like Java are often stack traces. The stack traces are often large and often have poor signal-noise. Important context is often missing--things like arguments to methods. C++ is sometimes worse, since there's a lot of C++ code out there which won't even give you a stack trace. Worst of all worlds, perhaps.
As I've gotten older, I've moved to a very proactive style of error checking. This style is verbose in all languages, and sometimes this style is much more natural in Go. That's because of the number of times that putting effort into error checking has saved my butt when debugging. By the time I'm debugging a piece of code, it might long forgotten. The error might be hard to reproduce. Getting a good report the first time saves a ton of time when debugging. In other languages, this might be accomplished by aggressive logging, or tools like time travel debugging, or catch & rethrow.
My sense of Go's error handling is that it is a lot of code only if you measure it by line count. The error handling is very boilerplate and you stop seeing it, like when Cypher's looking at code in The Matrix.
In isolated cases, it makes sense to panic() / recover() in Go. People new to Go sometimes overuse panic(), and then once they get more experience they overcompensate and don't ever panic()... I think there's a middle ground here, where panic()/recover() is useful in certain situations. I use it most for parsing / marshaling / unmarshaling code. Parsers, for example, can be very dense, and the error handling is often just "return to top, throw everything away, no additional context to report".
What we need is some education like "case studies for when you might want to panic/recover in Go" or "case studies in where you should add context to error messages".
Hard disagree. You lose a lot of context by simply returning an error message, which may not actually contain the relevant reason for the “non-happy path”.
While Java stack traces may be verbose, with even an ounce of experience one will realize that you only have to look at the top message and the “caused by” top messages. The other parts are important when you actually want to reproduce the error — good luck reproducing based on a single “failed to query user” message.
The point is that "simply returning an error message" is fine when you know that it actually contains the relevant reason.
You don't need to blindly annotate the reason for an error every single place an error gets returned. You often end up with redundant errors like this:
> could not load document "doc.txt": error reading document "doc.txt": could not open "doc.txt": open "doc.txt": file not found
So, sometimes you just return an error upwards, sometimes you conditionally wrap it depending on where the error came from, sometimes you conditionally wrap it depending on what the error type is.
And yes, you may be working with poorly-written code that doesn't do this, or you may be working with poorly-written code that just returns some fmt.Errorf() for everything... are you going to parse that? (My approach would be to fix the dependency so it returns better errors, if its needed for good error handling on my end.) Poorly written code is a hazard in all languages--I could just as easily throw RuntimeException in Java if I didn't care how it was handled.
> While Java stack traces may be verbose, with even an ounce of experience one will realize that you only have to look at the top message and the “caused by” top messages.
I can only conclude from this statement that I don't "even have an ounce of experience" with Java. That's extremely harsh! Maybe I'm just a complete idiot and it's hopeless, I should quit my job, or maybe I just haven't put in enough years working with Java.
My first complaint is that the context for an error is not present in the stack trace, because the stack trace only contains the location of the error, and does not tell you which objects the error is associated with (like, which user ID, just to make up an example). That information is either encoded in the exception message (it often isn't) or it is in the log somewhere, although you may not have an easy time finding the relevant line in the logs.
My second complaint is that the top and the bottom of the Java stack trace are usually not what you care about. You care about something near the top (but not at the top), some method which called a method that threw the exception, and you care about something farther down near the middle or bottom which shows the operation the program was doing (but not all the way at the bottom, because that will often just be some generic top-level try/catch).
> case studies in where you should add context to error messages
I'd love to see this. I feel like I often default to adding context to my errors, which is probably better than defaulting to not wrapping at all, but would love to know what criteria you use to determine when adding context adds value
> errors from GetUser would already say something like
GetUser can't know the call site or condition that led to the call and this is often the very thing you need. Amending returned errors with useful context is invaluable. Languages that permit this to be done concisely are a blessing: iFailed().expect("and this is why") for example.
> Java ... The stack traces are often large and often have poor signal-noise.
Earlier this week I needed the whole stack trace. The "who (ultimately) caused that to get called" question gets answered quickly by comprehensive stack traces.
I've actually used Java's stack trace behavior for reverse engineering. You can suss the design of some system by throwing something and then reading the stack trace, even when you lack the source code or the time the analyze it all. Good Java programmers know they can structure code specifically to increase the utility of stack traces.
> The error handling is very boilerplate and you stop seeing it
Wait, weren't we being made to think about it, as one of the supposed benefits? If, as you say, we won't see it then that argument goes out the window.
> error handling is three times the code line volume versus the core business logic. That's not ok
It is ok. That's really how it should be IMO. The "problem" is that Go doesn't allow you to say "eh, we'll just give a stack trace to the user, and then they can delete it print Oops something went wrong" which is apparently what most people want from their error handling.
I agree with the theoretical aspect of that position, except from what I’ve seen no one ends up testing the error paths. Untested error handling can have serious bugs that negate the whole value prop, so in practice the value is not as large as claimed.
Also, I think the other critique is less about forcing you and more about the syntax overload your force onto someone. Rust and swift come to mind as counter examples where you’re forced to handle every error AND the language tries to keep that out of normal code flow unless you are actually having to make some decision regarding the specific error.
As someone who really likes Go, I don't agree with you on this one. To me, it just means writing a lot of pointless boiler code and what's really worse is that the "happy path" of the code is obscured underneath a pile of pedantic make-work code
As another commenter mentioned, do you really think about them, or just tell yourself that you think about them while writing the same boilerplate millions of times?
Indeed. Also, this notion that Go makes one think about all the errors is false.
What happens to the errors from all the "defer foo.CloseOrMaybeFailSomehow()" that get run on early return? Answer: they're ignored. Ignoring errors baked right in. Yes, you can handle them with a closure, but I can count on part of one hand the number of times I've actually seen that. And I know why: because at some point the regression gets unreasonable and people just pretend they didn't see it and thank their makers the compiler lets them get away with it, this blog post in question being a fine example of that.
So I know what sort of malarkey I'm reading when this "think about the errors" gets bandied about.
Ok friend, but right here in this blog post is contrary evidence. The supposed "actual CopyFile function" ignores errors from two deferred .Close()s at the io.Copy() call site. [1]
And this is idiomatic Go. It goes on every day and you can find thousands of examples of it all over GitHub et al. Solving for it quickly leads to a real eye-sore: a deferred closure or similar blight.
So if "think about them" is your aspiration then Go has failed.
I don't think the article's `CopyFile` function is a good example for either `guard` statements or exceptions.
We can come up with better examples (feel free), but I'm going to focus on that since it's the one used in the article.
As a library consumer, I would like to know if the error happened in the source file or the destination file, ideally without requiring me to know implementation details of the function, so I don't have to worry if the implementation changes and now my `if errors.Is(err, fs.PathError)` no longer works and instead became a bug.
For example, the caller might want to:
* Show a user-friendly message to the user if the problem is in the source file.
* Return gracefully if there's not enough space to create the destination file (e.g. "copy as many files as you can to this drive, and tell me which ones you copied").
* "Bubble-up" the error in any other situation.
A function that lets me know which file was the cause of the problem might look like this:
Since now errors are actually useful to the caller function, the `guard` statements became unnecessary everywhere except in the `guard io.Copy(w, r)` case, because now we must handle nils (though I guess with generics we can create a wrapper that allows doing stuff like `guard WrapIfNonNil[ErrSourceFile](err)`, but whether that's better is debatable).
If we tried to do this with exceptions so that we can actually catch them, the code would become:
type SourceFileException exception
type DestFileException exception
func CopyFile(src, dst string) throws error {
// Now necessary because the `try` blocks introduce new scopes.
var (
r io.ReadCloser
w io.WriteCloser
)
try {
r = os.Open(src)
} catch err {
// Wrap exception because we don't want to hide
// the inner exception.
throw SourceFileException(err)
}
defer r.Close()
try {
w = os.Create(dst)
} catch err {
throw DestFileException(err)
}
defer w.Close()
io.Copy(w, r)
// The caller might want to check the hash to decide if it
// should delete the file or not.
try {
w.Close()
} catch err {
throw DestFileException(err)
}
}
... which is even more cumbersome than plain `if` blocks.
Exceptions have their advantages in certain cases, but they also have their downsides, it all depends on the situation and what you want to do.
And then elsewhere you call the function that contains that snippet, and return the error again.
I think the entire issue is that it looks too much like boilerplate. My preferred solution would be some operator that has sufficient expressiveness. After all, you should not only return an error, but there almost always are other return arguments as well, and sometimes another error should be returned, so it would look like this
db := openConnection(...) ~> {
error handling
}
In case of auto-zeroed or named return values, a simple ~> return would suffice.
But that's really not much better than the current situation. Go's syntax just resists functional solutions. Better leave it the way it is. In Go 1, certainly.
I really wish people would stop trying to "fix" Go error handling.
Go error handling is fine. If you want a terse language, use something else. Go error semantics have been this way for over a decade, they aren't going to (and shouldn't) change. The whole point of the current syntax is so that YOU DON'T IGNORE ERRORS. People keeps saying, "BUT MY LOC", just simply don't get it. You're supposed to do something with the error, either return it up the stack, or decorate it, or log it. Go didn't choose the exception route, stop trying to bolt exceptions on, and just adapt or stop using Go.
I don't think I give any more or less thought about it overall. Honestly, I'd probably rather just write the happy path, but still like the language either way.
In my experience, errors still get ignored - by accident. The err var juggling sometimes can't even be checked by a linter.
And of course it's just EXTREMELY verbose. Compare to rust, where it's not verbose at all, AND the type system is sufficient to force you to handle errors.
>>> When working in try/catch languages like JavaScript, I often easily forget which functions can throw. Even if I do remember, it’s easy to think “I think this gets caught somewhere up the call chain”.
...yeah, because what we really need are fckn CHECKED EXCEPION
I really never understood the gate for them. Exceptions can be thrown from a function? Then should be parti of the signature!!!
There are a lot of exceptions that can happen? Good, then keep track of them! Or handle them, or wrap them. With CHECKED exceptions there is no way to work only on the happy path
The one major implementation so far has been unwieldy and awful, and I’m not sure anyone has come up with a version which sucked any less.
Furthermore when you provide both checked and unchecked exceptions, it’s a lot smaller a step to switch from checked to unchecked than it is to switch from return errors to panics, and the “wrongness” is a lot less noticeable.
Until you change the function signature two libraries deep and break all code depending on it. It could work with only one exception type, incidentally go encourages this.
> I prefer how Go forces me to think about errors at every turn
Does it though, or does it just cause you to boilerplate `if err != nil {return err}` over and over?
In the example given, the author is simply manually rethrowing up the call chain to the layer that can handle it, it just takes three extra lines to do it, every single time
I agree, LoC is a very important measure of a language, not flexibility. If you really want to, just ignore the errors with "_" and let the runtime panic. Or maybe just use the language as intended.
I agree it’s be nice to be able to return error + zero values in a simpler way.
Personally I think they should add a keyword like returnif or check to do this, as sometimes (maybe half the time?) you don’t need to decorate an error if you want to handle it higher up. So you could just do:
returnif err
I hate the guard proposal as it mixes up error handling and function calls, but it’d be nice to have a simpler less verbose way to return errors.
In comparison with languages throwing exceptions though, Go is far far easier to reason about, even if more verbose.
Sorry, was commenting on the guard proposal not the article but yes you’re right about the article reiterating this but with 'guard' - this is an old idea that multiple people have proposed over the years usually with returnif or similar as the keyword.
Very confusing that he tried to repurpose ‘guard’ for something which is just a return.
That's rather dismissive. I use Go. I like it a lot, its a really practical language that helps me get a lot of stuff done. That's pretty much gold standard for programming languages to me. But it is clear that Go is stupidly wordy when dealing with errors and examining ways to fix that should be something we're open to.
FWIW I don't like the original proposal nor the article's solution to it
if error.Is(err, sql.ErrNoRow) {
return status.Error(codes.NotFound, "foo does not exist")
}
srv.log.Err(err).Msg("failed to load foo")
return status.Error(codes.InternalError, "")
True, that checking for more than one error type is very sparse but i still like the go errors the way they are.
There's nothing special about having different return types. Every language I can recall with throwable errors supports them, they just don't waste developer's time by manually bubbling them up the call stack.
Shouldn't go get rid of nil, perhaps by introducing sum types, before even thinking about error handling? I feel like that should be the priority personally
The two are married in my mind. Proper error handling with something that doesn't make my wrists ache just thinking about it falls out handily when combined with sum types.
Here is the same "actual CopyFile function" using a fictional Go Result type and a little sugar, which is what Go should be doing:
func CopyFile(src, dst string) Result<> {
r := os.Open(src)?
defer r.Close()?
w := os.Create(dst)?
defer w.Close()?
io.Copy(w, r)?
}
Yes I know this isn't likely to be the precise syntax; it's pseudocode for illustration only. Result<> would imply an "error", a miniscule adaptation for a working programmer.
Every case of err != nil has to be "thought about" or it won't compile. Nothing is ignored. Explicitness hasn't been reduced one meaningful iota. All the error handling branch noise is gone.
Retrofitting this onto Go is obviously not trivial. Obviously this won't work for all conceivable Go constructs. However, it will work almost all the time and immediately solve one of the worst aspects of Go.
Some of the claims in this thread are really frustrating. First, just because some people have a problem with the incessant "if (err != nil) { return err }" plaguing all Go code doesn't mean they want to "just ignore errors." They want a better solution. Second, the frequency of "if (err != nil) { return err }" does not indicate some failure on the part of the developer that they need to "fix." There is no fix in the prevailing Go language other than obfuscating code behind ever greater abstractions, the exact opposite of the intent of the Go programming language.
What's the benefit of the result type here, instead of keeping the "error" in the last spot? This would require refactoring everything, leaving you with "old style" and "new style" APIs.
Couldn't "?" just mean "strip the error", i.e. act as if the function only returns the value(s) it returns, and if the "?" "triggers", it would just return default/empty values for all parameters + the error in the error field. This also allows seamlessly mixing the abbreviated version and the full current approach if you need it for some reason.
I honestly also don't see how retrofitting it wouldn't be trivial. One could probably even write a preprocessor for that.
The most common argument I've heard is that it will lead to less explicit error messages because people will just pass through the original error instead of
so providing some convenient way to add this information would be great. Especially if it could be added once per function, and the whole system would preserve the stack trace like exceptions in any sane language do.
Just adding the stack trace alone will make the additional context a lot less important. I'd rather have human-written context and the stacktrace, but if I can have only one, I'd rather take the stack trace, especially since humans will be lazy and often omit the context.
Yeah, sure. I don't care. I'm just tired of typing and wading through if (err != nil) { return err } all day long. Go has generics, so why not magic the e r r o r into place and save the species a few billion keystrokes?
> I honestly also don't see how retrofitting it wouldn't be trivial.
I'm not the first to propose this. Others have and the naysayers are legion, citing one corner case after another. I argue that the corner cases aren't worth solving, perfect being the enemy of good. The complier is free to bail on whatever outlier gymnastics someone might try.
> One could probably even write a preprocessor for that.
I (and many others) abhor the concept of a precompiler/macrowhatever for Go (although some have resorted to them for reasons.) Go is supposed to be a fast and self contained; embiggening the tool chain with more stuff is the opposite of the intent of Go.
> it will lead to less explicit error messages
f := os.Open(savegamepath) ?("Couldn't load savegame: %s", err)
f := os.Open(savegamepath) ?{decorateAndReturnError(err) {...}}
Or similar perhaps.
Error handling is common. It's worth bending the rules a bit for a gain in concision. A little pragmatism would go a long way here.
All of this seems to be treating errors as nuisances. Go was written to treat errors as values. As someone who has written Go code professionally for more than a decade, I agree with that mantra and error checking is not really a pain point. I don't understand all the proposals that claim that it is...
The article as well as several comments repeat that “Go forces you to think about the error”. But isn’t that the opposite of what the tuple return pattern does? It allows you to ignore any error you want?
It’s just like an integer error code in C or bash, you check it when you remember but no one stops you from using the value even in the error case.
If the result and error was a single value (which I assume is the alternative here - not some Java-style exception system) then you’d be forced to consider the error.
The best you can do with a r, e tuple is checking if e isn’t nil over and over possibly cheered on by a linter?
That, IMO, keeps the spirit of (relative to, for example, JVM stack traces and try/catch) being in control of error reporting at the price of having to do more work getting decent partial stack trace-like information out of errors, but also removes a few pain points:
- it’s shorter (especially the single-line version)
- it limits the lifetime of ‘err’ to the ‘onError’ statement.
would be even shorter, but IMO would be a bit too magical for go, even if we made it clear magic is happening by giving that variable a special name, as shell does with ‘$?’.
The point of go hate always has been and will continue to be as a way of avoiding doing your job. Hating go is giving you an excuse to twiddle your thumbs and fantasize about impractical solutions (Rust)
145 comments
[ 3.5 ms ] story [ 210 ms ] threadI'm not a Goist, but I agree with TFA in general. Don't complicate a language for error handling purposes. My reasoning is that most errors cannot be properly dealt with anyway.
Is it the Erlang or the Elixir folks to just say, paraphrased, "let it die"?
Just let it blow up. Somewhere upstream can deal with it, because chances are we don't know enough down in the trenches to know what to do when something which we expect to work virtually always, fails.
And to improve the noise on the current copy example, I would argue to stop wasting lines!!! There is no good reason to open a bracket, newline, then return err, then close the bracket. It's small and obvious.
if err != nil { return err } // put it on one line. It's clean and natural.
Edit: Oh now I remember from my limited Go experience :(. They live by their formatter, and their formatter was built in a bondage dungeon.
An exception is something unexpected, unanticipated, exceptional. It's totally fine to crash on it. Running out of memory is a good example.
An error is something expected, a less probable but anticipated result. A request timeout is a good example. It's important to have a nice way to process it, and to take an action other than crashing, say, to retry.
In this regard, `guard` is a nice proposal: it explicitly shows where we are willing to crash (like `!` in Typescript or Rust or Kotlin), while staying perfectly backwards-compatible and not requiring any rewrites. It's not perfect in other respects though; I liked the `try` proposal better.
It's also very easy in many languages to use exceptions for non-"exceptional" control-flow even though they aren't really meant for that.
The biggest challenge with error -- or exception -- handling is that most callers are not prepared to deal with something other than success. So the safest thing for them to do is push the failure up, with the expectation that somewhere higher will be equipped to deal with it.
If you take TFA's CopyFile as an example, there are plenty of scenarios where copying a file would be part of some complex process. If the file copy fails, it probably indicates either a logic problem (bad path, empty filename, something) or an OS level failure. Either of those cases cannot be dealt with at layers close to the CopyFile call.
From TFA example, the guard only eliminates the if err != nil { return err }.
This is a bigger problem than can be solved with syntactic sugar. And from my perspective, it is why exceptions exist. If you merrily go on your way in some other languages and try to copy, but that fails, it will generate an exception. If you don't explicitly handle that exception in your calling function, then it will bubble up. If somewhere upstream expects this possibility, it can be designed to respond to a specific type of exception and do something reasonable about it. But your calling function, down here at a low level, will not have any clue what the business logic behavior should be if the CopyFile() fails.
Gopher.
> Is it the Erlang or the Elixir folks to just say, paraphrased, "let it die"?
That sounds like panic. When the Erlang folks say that, they're talking about letting a process die. The rest of the application is in different processes and doesn't get taken down. So the offending process can be rebooted and operations can continue.
> if err != nil { return err } // put it on one line. It's clean and natural.
Oh.. erm... well that's explicitly not letting it die. That's returning it and forcing it to be manually "dealt with", as you put it. It seems like you're contradicting yourself.
Also, the article explains that returning without context is often a bad idea.
Now I know what being old means. It means you can make references to things, and younger people will completely overlook them.
Of course I can do a small bit of internet search to get the answer to my rhetorical question. But I thought (wrongly) that it would be more entertaining to make a Monty Python reference. "Is your wife a goer? Know what I mean?"
> that's explicitly not letting it die. That's returning it and forcing it to be manually "dealt with"
That is letting it die in the sense of not handling the error locally. It is allowing the failure to go upstream, and either assuming the caller will handle it, blow up, or further pass it up.
> Erlang
As I understand it, the supervisor processes ensure that the workers are up and running. If one dies, it gets restarted within the constrants of failure count limits and such.
Instead of handling errors, you can choose to allow a failure to blow up the "process" (it's not a full OS process, I believe) and be comforted in the knowledge that your process will be restarted.
If you look at many business processes (take web app activities for example), you see that failures don't get resolved in perfect tidy fashion. Instead of trying to handle the multitude of possible failure cases, you just accept that many failures cannot be recovered from. You just let it silently die, and you maybe provide a general "sorry, try again" message to the user.
That is acceptable in a lot of cases, especially when it is virtually impossible to properly handle all failure cases gracefully. Imagine if a user were uploading an image to a web app, and CopyFile() failed. How would you handle that? Would you tell the user, "Sorry, we got your upload, but we couldn't put it in its proper place, so it failed. Try again."? Or could you let it "blow up" and just tell the user, "upload failed; try again".
Now imagine you try to handle failure cases and properly report them all upstream. Now if you write tests, your tests must test for the success path and multitudes of possible failure cases and resolutions. In practice, this just is not done (outside special high risk scenarios like commercial flight and such).
Gopher
Regarding the guard keyword, the author's proposal doesn't make sense. The guard call comes after the function call that produces the error, so it's not clear what is being guarded. I guess it's the error being wrapped in the guard message, but explaining that clearly in plain English is quite hard. I agree that context is important however. But maybe the Go team is finally ready to admit that stack traces are more useful than error messages. I'd be fine to know that, for example, an error occurred opening a file and having the language provide a full stack trace of where the error happened. I can do without the custom crafted error messages.
That’s all Must does - is make runtime panic look nice.
* MustCompile if you are unfamiliar - the compile refers to the compilation of the regular expression - which is always done at runtime.
Errors as values seems to sound better in theory then in practice, since in practice everything is type Error, and then I either expect enough from the error to do something about it, or I don't. Which... Is exactly how I use exceptions in Python, replete with mostly having no idea what will get thrown.
But Go makes it somewhat worse because most "error values" are just formatted strings, not actual types.
I really don’t think a quality gate is a problem, even if it’s sometimes inconvenient.
regexp.MustCompile exists to catch programmer mistakes. You've presumably done your testing and when you ship the software you believe the regexp is correct and it should be impossible to fail, so if it still fails, something exceptional has happened; an exception as we call them in the biz.
What you are doing when reading a config file is dealing with user mistakes. These are not exceptional, they are very much expected to happen and when they do you would traditionally want to provide useful feedback to the user, not simply crash.
This may be something that people do, but it isn't what you'd want them to do. panic/recover are intended for exceptions, not errors. Why make it easier for developers to do silly things? In the rare cases where you actually have exceptions to deal with, a little extra work to make the situation clear to the reader is okay.
If you were loading the regexp from an alterable file at runtime, to allow the user to change it after the program is shipped, you wouldn't use regexp.MustCompile. You would use regexp.Compile and gracefully deal with any errors that the user may have made.
If you are talking about compile-time configuration, where once your program is built it won't change, why would you use YAML when you could simply use language-native variables/constants with actual compile-time safety? Regexps can improve on some programming problems over writing the equivalent code, so it's a good tradeoff in some cases. YAML is never nicer than the equivalent code.
The proposal the submitted article references has been inactive for years.
After submitting proposals and looking at the existing ones and thinking about this more I think the biggest problem with error verbosity is just that you have to also return zero values for the success results when all you want to do is return an error. This obfuscates the intent, making things harder to read and write. In some scenarios such as adding an additional return result it forces changes on multiple unrelated lines when it should just be a single-line change.
It's also possible that errors wouldn't be as verbose if Golang had automatic error tracing like Zig (a slightly better form of stack traces)- in that case the need for error annotations would decrease. With that and zero values removed, just allowing the error return on a single line instead of 3 might be all that is needed.
Normally one would not compare two interface values, so this issue really comes up when comparing an interface to nil. If Go had proper option types then this issue would go away.
https://go101.org/article/nil.html
package main
import "fmt"
type Err1 struct{}
func (m Err1) Error() string { return "" }
type Err2 struct{}
func (m Err2) Error() string { return "" }
func main() {
Nope. This isn't even true in all situations. For a simple example, just look at description of the extremely popular io.Reader:
> An instance of this general case is that a Reader returning a non-zero number of bytes at the end of the input stream may return either err == EOF or err == nil.
https://godocs.io/io#Reader
I agree, and this got worse with generics since T{} doesn't work, and we have to use *new(T), which is quirky.
However, I wonder if we just want an Optional/Maybe type that has support for multiple values. Without variadic/recursive generics, we'd need compiler support. But a tagged union would get rid of the zero-values.
(I somewhat agree about the io.Reader case where returning both the number of "screwed up" bytes and an error can be useful, though perhaps the number of bytes should be encoded in the error type instead. I rarely care about them.)
Or you can just keep it as-is because it's one of the very few cases where it does make sense to have a product type.
https://github.com/golang/go/issues/19642 was a proposal to have e.g.
as an alternative to e.g.No additional cognitive load on figuring out what's going on behind the scenes on errors. Priceless.
It's all an incredible amount of effort to avoid what boils down to a basic nil pointer check. I agree the blocks are unsightly, but they're as plain and flat as you could possibly hope for. I almost think you'd be better off using syntax highlighting to obscure them if they're that painful to have to look at.
Also it has a bit of educational component. It's like a seatbelt. In some societies people consider seatbealts annoying and irritating thing that restrict their freedom. They use seat belt plugs to get rid of that annoyance. Go just forces you to use it, regardless of your initial predisposition to the seatbelts.
Phrases like "noise created by error handling" start sound like "the incovenience created by seatbelt". It's not noise.
Back then in C++ times, I also felt like 10+ min compilation times were non-issue. Until I experienced subsecond compilation times, of course.
I love golang as much as I hate its error handling. If not for it, I'd switch to it tomorrow. As it is now, I'd rather bend Java to be golang. Just because of error handling.
The only thing that I'd add to Java exceptions is to enrich the exception stacktrace with all serializable local variables on every level (including parameters). Yes, that's a lot of data and some smart approach should be taken to serialize it to string. Basically all "simple" local variables should be included to stacktrace. That'd be another level of error reporting.
the fact that the most popular Go IDE actually has logic to automatically fold and hide the constant error handling boilerplate and that people seem to have no problem with that makes me feel like I'm taking crazy pills.
C++ and Java exceptions have non-local flow and it's hard to determine what function can throw what, so if you do want to add error handling cases you would have to inspect every function to know if it throws or not. Yeah, there's noexcept and checked exceptions but one was a late addition that legacy codebases don't use and the other everyone just does throw new RuntimeException(err) anyway.
In general I agree with you, but Rust is already having this problem by people making liberal use of unwrap.
Plus, most things that work like "open" close when their scope closes, so you don't need all that "defer" stuff.
I would have liked a good error hierarchy like Python 2 had, where there was a clear distinction between program-is-broken errors and errors due to external causes. But Rust didn't get that at the beginning, and now it's too late.
anyhow::Error really does a good job of smoothing out the finicky bits of Rust error handling, it feels and reads a lot better than Go code to me.
(And I’m not sure about the local variables idea, because that could become really slow and could effectively duplicate the stack in size, or even more in the case of “suppressed” exceptions).
Those are just objective facts. I, personally, don’t like this feature either. May be with very different language design it could be better, but as it is, it’s ergonomics is awful IMO. And I have problems to imagine how to rework it to make it work.
Java developers are really divided on the topic of checked exceptions, there is no consensus.
Go allows you to do that with panic/recover, although it's not quite idiomatic
Writing boilerplate error handling code for every function call is one of those things that can make you _think_ you're bring productive because you're writing a lot of code, when in reality it's mostly just tedious mechanical work of little value.
Swift does an okay job at this. Java forces you to handle _most_ but not all exceptions.
Sugar for error handling is already a solved problem (.e.g: Rust, Swift, or Zig).
Go will be damned by the community's NIH syndrome.
Sum types + pattern matching + tuples alone are so powerful, they could fix like top 5 of my biggest annoyances with Go.
Add some syntax sugar (like '?' from Rust), and it fixes a majority of the boilerplate issues.
Imo, people praise Rust too much for the ownership and safety story, and way to little for its (more simplistic) excellent decisions around simple types.
Long before Rust I used to implement this (to the extent possible) with templates in C++ when I'd start work on a new code base, if it didn't exist. With generics added to Go I might start the same at my current role. It's too bad there aren't other tools to support it (e.g. macros).
It would not be as bad if they were typed at least. My main pain point of Go error handling is that it's all `error` everywhere, and you have to read docs or even library code to understand what kind of errors there can be and how to handle them.
Sure, in many cases you’re going to bubble up the error (perhaps a syntactic sugar for this scenario would help), but there are lots of production error handling considerations like transaction rollbacks, response statuses, logging, metrics, opentelemetry traces, retry logic, that you will need to handle in practice, and you will want to handle these differently in different failure modes.
Ensuring software acts in an expected way in the transient 0.001% scenarios is very important. With Go I never run into unhandled exceptions causing software to blow up like I do with large Python codebases.
At first I didn’t like the “if err != nil” bloat of Go code, but now I appreciate its explicitness, it is distinctly unmagical, simple, boring, and works well in production.
No there's not. People seem to just want to ignore all errors, and let the language "figure it out", AKA exceptions. If you want to ignore errors, then just go all the way and do that:
then, you never have to worry about errors, you can just code in bliss and wait for the "panic" because you didn't want to bother with proper error handling.I have personal experience with Java, Go, and C#, to varying degrees. Over the years, my personal style has changed a lot, based on my experience running applications (as someone in ops), developing applications, debugging applications, etc.
First of all, I'll say that doing Go error handling well figuring out the right place to add context to your errors. So in the example above:
The error is from a method named "GetUser", and in the style guide I use, errors from GetUser would already say something like "get user %d: %w". You'd know this when writing code that calls GetUser, and wouldn't double-wrap it with additional context, if the additional context is not useful.That's a bit of a tangent.
My experience working in operations is that errors in languages like Java are often stack traces. The stack traces are often large and often have poor signal-noise. Important context is often missing--things like arguments to methods. C++ is sometimes worse, since there's a lot of C++ code out there which won't even give you a stack trace. Worst of all worlds, perhaps.
As I've gotten older, I've moved to a very proactive style of error checking. This style is verbose in all languages, and sometimes this style is much more natural in Go. That's because of the number of times that putting effort into error checking has saved my butt when debugging. By the time I'm debugging a piece of code, it might long forgotten. The error might be hard to reproduce. Getting a good report the first time saves a ton of time when debugging. In other languages, this might be accomplished by aggressive logging, or tools like time travel debugging, or catch & rethrow.
My sense of Go's error handling is that it is a lot of code only if you measure it by line count. The error handling is very boilerplate and you stop seeing it, like when Cypher's looking at code in The Matrix.
https://www.youtube.com/watch?v=MvEXkd3O2ow
In isolated cases, it makes sense to panic() / recover() in Go. People new to Go sometimes overuse panic(), and then once they get more experience they overcompensate and don't ever panic()... I think there's a middle ground here, where panic()/recover() is useful in certain situations. I use it most for parsing / marshaling / unmarshaling code. Parsers, for example, can be very dense, and the error handling is often just "return to top, throw everything away, no additional context to report".
What we need is some education like "case studies for when you might want to panic/recover in Go" or "case studies in where you should add context to error messages".
While Java stack traces may be verbose, with even an ounce of experience one will realize that you only have to look at the top message and the “caused by” top messages. The other parts are important when you actually want to reproduce the error — good luck reproducing based on a single “failed to query user” message.
You don't need to blindly annotate the reason for an error every single place an error gets returned. You often end up with redundant errors like this:
> could not load document "doc.txt": error reading document "doc.txt": could not open "doc.txt": open "doc.txt": file not found
So, sometimes you just return an error upwards, sometimes you conditionally wrap it depending on where the error came from, sometimes you conditionally wrap it depending on what the error type is.
And yes, you may be working with poorly-written code that doesn't do this, or you may be working with poorly-written code that just returns some fmt.Errorf() for everything... are you going to parse that? (My approach would be to fix the dependency so it returns better errors, if its needed for good error handling on my end.) Poorly written code is a hazard in all languages--I could just as easily throw RuntimeException in Java if I didn't care how it was handled.
> While Java stack traces may be verbose, with even an ounce of experience one will realize that you only have to look at the top message and the “caused by” top messages.
I can only conclude from this statement that I don't "even have an ounce of experience" with Java. That's extremely harsh! Maybe I'm just a complete idiot and it's hopeless, I should quit my job, or maybe I just haven't put in enough years working with Java.
My first complaint is that the context for an error is not present in the stack trace, because the stack trace only contains the location of the error, and does not tell you which objects the error is associated with (like, which user ID, just to make up an example). That information is either encoded in the exception message (it often isn't) or it is in the log somewhere, although you may not have an easy time finding the relevant line in the logs.
My second complaint is that the top and the bottom of the Java stack trace are usually not what you care about. You care about something near the top (but not at the top), some method which called a method that threw the exception, and you care about something farther down near the middle or bottom which shows the operation the program was doing (but not all the way at the bottom, because that will often just be some generic top-level try/catch).
I'd love to see this. I feel like I often default to adding context to my errors, which is probably better than defaulting to not wrapping at all, but would love to know what criteria you use to determine when adding context adds value
GetUser can't know the call site or condition that led to the call and this is often the very thing you need. Amending returned errors with useful context is invaluable. Languages that permit this to be done concisely are a blessing: iFailed().expect("and this is why") for example.
> Java ... The stack traces are often large and often have poor signal-noise.
Earlier this week I needed the whole stack trace. The "who (ultimately) caused that to get called" question gets answered quickly by comprehensive stack traces.
I've actually used Java's stack trace behavior for reverse engineering. You can suss the design of some system by throwing something and then reading the stack trace, even when you lack the source code or the time the analyze it all. Good Java programmers know they can structure code specifically to increase the utility of stack traces.
> The error handling is very boilerplate and you stop seeing it
Wait, weren't we being made to think about it, as one of the supposed benefits? If, as you say, we won't see it then that argument goes out the window.
It is ok. That's really how it should be IMO. The "problem" is that Go doesn't allow you to say "eh, we'll just give a stack trace to the user, and then they can delete it print Oops something went wrong" which is apparently what most people want from their error handling.
Also, I think the other critique is less about forcing you and more about the syntax overload your force onto someone. Rust and swift come to mind as counter examples where you’re forced to handle every error AND the language tries to keep that out of normal code flow unless you are actually having to make some decision regarding the specific error.
The particular syntax for returning an undecorated error is needlessly verbose and takes three lines.
We shouldn’t have to write this say three times in a function:
if err != nil {
return nil, 0, err
}
What happens to the errors from all the "defer foo.CloseOrMaybeFailSomehow()" that get run on early return? Answer: they're ignored. Ignoring errors baked right in. Yes, you can handle them with a closure, but I can count on part of one hand the number of times I've actually seen that. And I know why: because at some point the regression gets unreasonable and people just pretend they didn't see it and thank their makers the compiler lets them get away with it, this blog post in question being a fine example of that.
So I know what sort of malarkey I'm reading when this "think about the errors" gets bandied about.
Ok friend, but right here in this blog post is contrary evidence. The supposed "actual CopyFile function" ignores errors from two deferred .Close()s at the io.Copy() call site. [1]
And this is idiomatic Go. It goes on every day and you can find thousands of examples of it all over GitHub et al. Solving for it quickly leads to a real eye-sore: a deferred closure or similar blight.
So if "think about them" is your aspiration then Go has failed.
[1] https://pkg.go.dev/os#File.Close
We can come up with better examples (feel free), but I'm going to focus on that since it's the one used in the article.
As a library consumer, I would like to know if the error happened in the source file or the destination file, ideally without requiring me to know implementation details of the function, so I don't have to worry if the implementation changes and now my `if errors.Is(err, fs.PathError)` no longer works and instead became a bug.
For example, the caller might want to:
* Show a user-friendly message to the user if the problem is in the source file.
* Return gracefully if there's not enough space to create the destination file (e.g. "copy as many files as you can to this drive, and tell me which ones you copied").
* "Bubble-up" the error in any other situation.
A function that lets me know which file was the cause of the problem might look like this:
Since now errors are actually useful to the caller function, the `guard` statements became unnecessary everywhere except in the `guard io.Copy(w, r)` case, because now we must handle nils (though I guess with generics we can create a wrapper that allows doing stuff like `guard WrapIfNonNil[ErrSourceFile](err)`, but whether that's better is debatable).If we tried to do this with exceptions so that we can actually catch them, the code would become:
... which is even more cumbersome than plain `if` blocks.Exceptions have their advantages in certain cases, but they also have their downsides, it all depends on the situation and what you want to do.
As everything, it's a trade-off.
I think the entire issue is that it looks too much like boilerplate. My preferred solution would be some operator that has sufficient expressiveness. After all, you should not only return an error, but there almost always are other return arguments as well, and sometimes another error should be returned, so it would look like this
In case of auto-zeroed or named return values, a simple ~> return would suffice.But that's really not much better than the current situation. Go's syntax just resists functional solutions. Better leave it the way it is. In Go 1, certainly.
Go error handling is fine. If you want a terse language, use something else. Go error semantics have been this way for over a decade, they aren't going to (and shouldn't) change. The whole point of the current syntax is so that YOU DON'T IGNORE ERRORS. People keeps saying, "BUT MY LOC", just simply don't get it. You're supposed to do something with the error, either return it up the stack, or decorate it, or log it. Go didn't choose the exception route, stop trying to bolt exceptions on, and just adapt or stop using Go.
if err := c.ShouldBindHeader(&headers); err != nil { log.Println(err.Error()) c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return }
I don't think I give any more or less thought about it overall. Honestly, I'd probably rather just write the happy path, but still like the language either way.
And of course it's just EXTREMELY verbose. Compare to rust, where it's not verbose at all, AND the type system is sufficient to force you to handle errors.
>>> When working in try/catch languages like JavaScript, I often easily forget which functions can throw. Even if I do remember, it’s easy to think “I think this gets caught somewhere up the call chain”.
...yeah, because what we really need are fckn CHECKED EXCEPION
I really never understood the gate for them. Exceptions can be thrown from a function? Then should be parti of the signature!!!
There are a lot of exceptions that can happen? Good, then keep track of them! Or handle them, or wrap them. With CHECKED exceptions there is no way to work only on the happy path
The one major implementation so far has been unwieldy and awful, and I’m not sure anyone has come up with a version which sucked any less.
Furthermore when you provide both checked and unchecked exceptions, it’s a lot smaller a step to switch from checked to unchecked than it is to switch from return errors to panics, and the “wrongness” is a lot less noticeable.
Does it though, or does it just cause you to boilerplate `if err != nil {return err}` over and over?
In the example given, the author is simply manually rethrowing up the call chain to the layer that can handle it, it just takes three extra lines to do it, every single time
That is a completely false dichotomy. You can provide terseness for the common case while allowing flexibility for when that’s warranted.
You can even artificially increase verbosity as disincentive for constructs which are necessary but should be avoided.
Personally I think they should add a keyword like returnif or check to do this, as sometimes (maybe half the time?) you don’t need to decorate an error if you want to handle it higher up. So you could just do:
returnif err
I hate the guard proposal as it mixes up error handling and function calls, but it’d be nice to have a simpler less verbose way to return errors.
In comparison with languages throwing exceptions though, Go is far far easier to reason about, even if more verbose.
That's exactly his proposal at the end of the article, except he uses 'guard' as the keyword.
Very confusing that he tried to repurpose ‘guard’ for something which is just a return.
if error.Is(err, sql.ErrNoRow) { return status.Error(codes.NotFound, "foo does not exist") } srv.log.Err(err).Msg("failed to load foo") return status.Error(codes.InternalError, "")
True, that checking for more than one error type is very sparse but i still like the go errors the way they are.
Instead of having programmers slog through an if statement:
Allow them to instead write [the superior]: PROS:* composes well with go’s existing “errors as values” philosophy
* matches →‘s usage in formal logic to signify implication
* a whopping 50% less verbose than “if”
CONS:
* to make it backwards compatible, we’d need to also add “←”
Every case of err != nil has to be "thought about" or it won't compile. Nothing is ignored. Explicitness hasn't been reduced one meaningful iota. All the error handling branch noise is gone.
Retrofitting this onto Go is obviously not trivial. Obviously this won't work for all conceivable Go constructs. However, it will work almost all the time and immediately solve one of the worst aspects of Go.
Some of the claims in this thread are really frustrating. First, just because some people have a problem with the incessant "if (err != nil) { return err }" plaguing all Go code doesn't mean they want to "just ignore errors." They want a better solution. Second, the frequency of "if (err != nil) { return err }" does not indicate some failure on the part of the developer that they need to "fix." There is no fix in the prevailing Go language other than obfuscating code behind ever greater abstractions, the exact opposite of the intent of the Go programming language.
Couldn't "?" just mean "strip the error", i.e. act as if the function only returns the value(s) it returns, and if the "?" "triggers", it would just return default/empty values for all parameters + the error in the error field. This also allows seamlessly mixing the abbreviated version and the full current approach if you need it for some reason.
I honestly also don't see how retrofitting it wouldn't be trivial. One could probably even write a preprocessor for that.
The most common argument I've heard is that it will lead to less explicit error messages because people will just pass through the original error instead of
so providing some convenient way to add this information would be great. Especially if it could be added once per function, and the whole system would preserve the stack trace like exceptions in any sane language do.Just adding the stack trace alone will make the additional context a lot less important. I'd rather have human-written context and the stacktrace, but if I can have only one, I'd rather take the stack trace, especially since humans will be lazy and often omit the context.
Only to be consistent with the more general case:
Or perhaps you mean why not: Yeah, sure. I don't care. I'm just tired of typing and wading through if (err != nil) { return err } all day long. Go has generics, so why not magic the e r r o r into place and save the species a few billion keystrokes?> I honestly also don't see how retrofitting it wouldn't be trivial.
I'm not the first to propose this. Others have and the naysayers are legion, citing one corner case after another. I argue that the corner cases aren't worth solving, perfect being the enemy of good. The complier is free to bail on whatever outlier gymnastics someone might try.
> One could probably even write a preprocessor for that.
I (and many others) abhor the concept of a precompiler/macrowhatever for Go (although some have resorted to them for reasons.) Go is supposed to be a fast and self contained; embiggening the tool chain with more stuff is the opposite of the intent of Go.
> it will lead to less explicit error messages
Or similar perhaps.Error handling is common. It's worth bending the rules a bit for a gain in concision. A little pragmatism would go a long way here.
If the result and error was a single value (which I assume is the alternative here - not some Java-style exception system) then you’d be forced to consider the error.
The best you can do with a r, e tuple is checking if e isn’t nil over and over possibly cheered on by a linter?
- it’s shorter (especially the single-line version)
- it limits the lifetime of ‘err’ to the ‘onError’ statement.
It also, IMO, reads easier than these proposals.
An implicitly declared ‘err’, as in
would be even shorter, but IMO would be a bit too magical for go, even if we made it clear magic is happening by giving that variable a special name, as shell does with ‘$?’.