"Whatever you do, always check your errors!"
"Whatever you do, always check your errors!"
Addendum: It is still a good thing to be able to add context to the error message though, so I think you've got a point.
But part of me also sees the Go's team point on this, which is that not all functions always need their error checked - as an obvious example, fmt.Println.
Sure, the compiler could add explicit exceptions for those cases, but that's a very unclean solution and it doesn't handle third party libraries.
The "Go way" is to use the errcheck tool to do that.
And on balance I think that having that defined in a separate tool - which can be configured by the user to exclude modules of their choice, and comes with good defaults - is the correct choice.
I agree, but a lot of other languages handle this much better. In Rust (predictably!) you receive a Result type, and if you don't want to check the error you either .unwrap() it or apply the ? operator to pass it up.
I feel like making "let's not check this error" explicit rather than implicit would be an improvement. Currently in Go it's impossible to tell whether someone forgot to check an error, or if they omitted the check intentionally.
As an aside: I feel like if you are ever in a situation in which fmt.Println returns an error, then whatever situation you are in is already far, far beyond saving. Maybe fmt.Println should just panic on an error, that seems better than output silently being dropped!
Not necessarily, it could just be that the user has closed the stream for one reason or an other. Possibly because they’re running it as a service without having set up an stdout, if the program has useful side effects.
How, exactly, does one find themselves in a situation where they forget to add entire blocks of logic to their application and not notice? We're not exactly talking about subtle bugs here. This is completely missing functionality – something that becomes immediately obvious as soon as testing begins.
Which, I guess, means that the previous developer did no testing at all. In which case, where do you even begin to figure out what else they have forgotten? Such a codebase, no matter the language, may not even be salvageable at that point.
It automates checking but not handling the errors. It's the Go equivalent of an empty catch{} block. It allows a programmer to not care about errors, which works out to the same thing as ignoring them.
Would probably help if you did not need an external linter to remind you. Alas, here as well Go is all hat.
That's true if the function doesn't return a value (other than the error) or if it does and the caller also ignores the return value.
That is, given
func F() error {
...
}
It's legal to call F() without checking the return. However in the case that the function returns a value and an error, like so func G() (int, error) {
...
}
then it's fine to just call G() and ignore anything returned, but i := G()
will cause a compile error. It's possible to assign one or both values to the Blank Identifier, the underscore _, so ignoring the error, while possible, requires the code to reflect the intent, like so i, _ := G()
but i, err := G()
will fail to compile if either i or err are not used following the call.https://go.dev/play/p/Z8xNWiJHPV0
Maybe the compiler should require the error return to be assigned for F(), that's a bit of a quirk that's under discussion.
“Yes” would have done just fine, especially as this is not uncommon when it comes to IO. But then again who’d do IO in Go right?
> will fail to compile if either i or err are not used following the call.
a, err := G()
if err != nil {
panic(err)
}
i, err := G()
i, err = G()
err = F()
fmt.Println("Got", a, i)
> Maybe the compiler should require the error return to be assigned for F(), that's a bit of a quirk that's under discussion.It’s not “a bit of a quirk”. The langage only checks for unused variables which is woefully insufficient and unfit for purpose, and has been since the langage was first released.
1. it's very very tedious to have two unrelated error systems in one language (yours and the one everyone else in C# uses) 1. as Go demonstrates, this type of error handling is extremely tedious even by itself
But is the tedium worth it?
But, exactly like you say, you end up fighting libraries - particularly the standard libraries, and it ain’t a fight you will win.
In typescript it actually kind of works - better at least. JS APIs often are errors-as-values already because of the old continuation async APIs, and typescript of course lends itself incredibly well to rich return types.
To me part of a language's utility is measured by how well it works in the group setting. That's what typically drives the industry, and more to the point for me it is the situation I will encounter most of the time.
I hear this often from Go advocates, but as someone who cut my teeth in Java in 2004, I've only ever seen this done in one codebase: an old streaming parser that I haven't seen since, and where the alternatives were, at the time, generally _more_ confusing than tossing an "unexpected end of input" exception that contained context.
In the 19 years since (14 of which have been my professional career, mostly with Java as part of the job _somewhere_), I've never seen it since.
In the meantime, though, I've run into better, more-explicit error-handling strategies (monadic errors, union types with explicit unwrapping) that manage to make the usually-bad option ("I'm swallowing this error") explicit and intentional, which is not something Go manages to achieve. (Interestingly, these better approaches all pre-date Go, so it's not like there wasn't a better state of the art to learn from.)
But Go also doesn't make error-propagation easy either.
Somehow, Go manages to optimize for accidentally swallowing errors, which is sort of impressively bad. It reminds me of something like INTERCAL: engineered to be as much a footgun as possible. INTERCAL is a parody, though, so it has that going for it.
How so?
I present to you Sneaky Throw, credit to the author in [1]
public class Sneak {
public static RuntimeException sneakyThrow(Throwable t) {
if ( t == null )
throw new NullPointerException("t");
Sneak.<RuntimeException>sneakyThrow0(t);
return null;
}
@SuppressWarnings("unchecked")
private static <T extends Throwable> void sneakyThrow0(Throwable t) throws T {
throw (T)t;
}
}
Now you too can force your callers to accidentally swallow errors and have the code compile ;)[1] https://www.mail-archive.com/javaposse@googlegroups.com/msg0...
The much-maligned checked exceptions, obviously, _require_ you to have a "catch" block for the exceptions in question, or else you get a compiler error.
Option types, Result types, and Either types (which are just generalized Result types) _require_ you to unwrap them explicitly, or else you'll get a compiler error because a Result<T> is not a T.
In Haskell, you've got monadic error-handling inside of do-notation, which is implicit, but at least does the right thing by default of propagating the error back to you, rather than defaulting to swallowing it and moving on.
Meanwhile, in golang, you write this form around 6-7 times in any function of more than a few lines:
result, err := someFunctionCall(input)
if err != nil {
return nil, err
}
...sure, that's so much noise it's hard to miss.....the first time. But since that's literally the only way errors can be handled, you wind up with something more like this: request, err := readHttpRequest(inputStream)
if err != nil {
return nil, err
}
userSubmission, err := parseUserSubmission(request)
if err != nil {
return nil, err
}
err = validateUserSubmission(userSubmission)
userSubmission = formatAndTruncateMessageText(userSubmission)
submissionTimestamp, err := clock.currentTimeMillis()
if err != nil {
return nil, err
}
insertedId, err := saveUserSubmission(userSubmission, submissionTimestamp)
return formatResponse(userSubmission, insertedId, submissionTimestamp)
...how quickly can you spot the error that was swallowed? How quickly could you spot it at 2am when another, downstream service is broken because its submissions are failing validation but the validation error isn't propagated?By taking away the typesafety of requiring some sort of type wrapper for multiple return that must be unwrapped, _and_ by taking away the enforcement that you have to check for and either propagate or explicitly swallow the error (by way of a result type or even checked exceptions), Golang takes away your guardrails, leaving you on the mountainside and liable to fall off easily.
By making you do repetitive boilerplate "if err != nil { return nil, err }" every other line or so (rather than providing automatic error-propagation machinery like Haskell's do-notation or Rust's `?` operator), Golang lulls you into "highway hypnosis"[1], setting you up to be much more likely to accidentally drive over the cliff. It makes the Right Thing™ tedious, easily omitted, and only enforced by your own constant vigilance (or complex external tooling that has to guess at your intent), and makes the Wrong Thing™ the default.
And yet, Golang supports Goto.
Also, just because your (not talking to you, just a pet-peeve of mine) CS101 professor said that Goto's are bad, doesn't mean it is true in 100% of cases.
I've actually never ever seen that done in production code in the last 20 years I've been programming. I've only seen exceptions used for errors in which case the described behaviour is exactly what I want.
The fact that Go advocates have to exaggerate the issues with exceptions to make the design of errors in Go seem reasonable makes me extremely suspicious.