>So long as we can all agree that it feels super bad, I guess this is fine.
Actually, I don't think everyone agrees it feels super bad. I personally like having all of my error handling be explicit, painfully explicit even.
>approaches the Java world back with checked exceptions where principle trumped ergonomics.
I also have to disagree here. To me, checked exceptions are the worst of both worlds. Here you have additional language features, but nearly the same verbosity. Your 'happy path' must be surrounded by try blocks. Worse, everything that happens in a try block is 'flattened.' Not only does this mean you may need many try blocks, sometimes it can even be difficult to take individual statements and put them in one try block. For example:
try {
doThing(doOtherThing());
} catch(...) {
// What happens if doThing and doOtherThing throw the same exception? Do I have to use a temporary variable?
}
Also, exceptions have been overloaded to handle everything, including runtime errors. I think this is more subjective but I strongly dislike it. I do think runtime errors should be possible to handle, just ideally through a separate, more explicit paradigm.
try {
doThing(blah[0]);
} catch(...) {
...
} catch(IndexOutOfRange) {
// Handling runtime errors at the same level as application-level errors!!
}
Go doesn't even really force you to check your errors, it just makes it harder to accidentally not check them. Like:
result, err := doThing(); // Error
return result
result, _ := doThing(); // NOT an error
return result
I think this is excellent. It may lead to complaints about an annoying compiler, but most importantly it leads to fewer mistakes. You can still explicitly tell the compiler to shut up, but it is obvious.
Language ergonomics are complicated, but the benefits of Go's approach are hard to deny. Unfortunately, nothing comes without downsides, and it seems like solving the error handling ergonomics issue is a tough one. I think repetitiveness aside, the Go error handling ergonomics are great, and that's exactly why they attempted to reduce repetitiveness. But, in reflection, a lot of that repetitiveness can also be reduced by refactoring your code, so it may not even be quite as bad as it seems.
> Worse, everything that happens in a try block is 'flattened.' Not only does this mean you may need many try blocks, sometimes it can even be difficult to take individual statements and put them in one try block.
Go has exactly the same issue. But with exceptions at least you can group multiple statements together and handle them with one catch block. With Go you have to use multiple if blocks to get the same semantics.
> Go doesn't even really force you to check your errors, it just makes it harder to accidentally not check them.
Go doesn't make it harder to accidentally not check errors. If you call a function that only returns an error, such as os.Mkdir(), then the compiler will not warn you when you forget to handle the error.
> Language ergonomics are complicated, but the benefits of Go's approach are hard to deny.
I don't really see any benefit to Go's approach over exceptions or result types. If making it obvious that errors are handled is important, there's a solution for that that's much more elegant than if-err-nil blocks everywhere. It's precisely the solution that the Go community just rejected.
>Go has exactly the same issue. But with exceptions at least you can group multiple statements together and handle them with one catch block. With Go you have to use multiple if blocks to get the same semantics.
Go does not suffer from this issue at all. I am not talking about flattening from the call hierarchy, I am talking about flattening the try scope itself.
(In case it isn’t obvious: in Go you’d be forced to separate two error checks. But it results in still less cumbersome code, since you only need scoping for the error handling portions.)
But forgetting that, because passing errors down in Go is explicit, it is actually customary to use error wrapping to add context as an error is propagated, which does allow for more precise error handling actually.
>Go doesn't make it harder to accidentally not check errors. If you call a function that only returns an error, such as os.Mkdir(), then the compiler will not warn you when you forget to handle the error.
Go vet will do that. It is a good 'first step' to configure when setting up your CI/CD (many will do it by default.) Go vet's 'unusedresult' checker does this.
>I don't really see any benefit to Go's approach over exceptions or result types.
It is a simpler language than Java. You get most of the benefits of checked exceptions without all of the calories from exceptions.
> But it results in still less cumbersome code, since you only need scoping for the error handling portions.)
There isn't a meaningful difference in cumbersomeness between having two try-catch blocks and two if-err blocks. There is a meaningful difference in cumbersomeness between what Go has today and "try foo(try bar())". Which is why it's so unfortunate that the community killed the try proposal.
> But forgetting that, because passing errors down in Go is explicit, it is actually customary to use error wrapping to add context as an error is propagated,
People say this, but in practice any code search reveals that "if err != nil { return err }" is everywhere. I believe that many Go projects aspire to annotate all errors, but much fewer actually do.
Ironically, the nice thing about exceptions, as well as Rust error chaining crates, is that they do this automatically, so in practice programs written in languages with those features tend to have better error diagnostics than Go programs do. Computers are better at doing things consistently than humans are.
> It is a simpler language than Java. You get most of the benefits of checked exceptions without all of the calories from exceptions.
Go is a more complex language than Java is overall, because of all the special cases the language adds to magic types like maps and errors that are just part of the library in Java.
> any code search reveals that "if err != nil { return err }" is everywhere
Code searches in languages with exceptions tend to wrap the tryblocks around massive portions of code instead of the individual function calls to the point that you have a top level doing:
But that is exactly how error handling usually works, especially if cleanup is handled separately - you just need to propagate the errors, usually all the way up to the user, who is the only one who can take a meaningful decision.
Almost all actual error handling in code is either error translation and re-throw, resource cleanup, or automatic retries (sometimes you retry the same request, sometimes you try a fallback option, but it's still the same idea).
The user however may be able to check and actually fix their internet connection, they may fix the typo they did in the config file, they may call support to see what's happening to the database etc. - your program can't do any of these things.
That's why exceptions work so well in most languages, especially GC languages where you have dramatically fewer resources to cleanup: they bubble up automatically towards the initial caller, which is often the user. Threading messes with this, but if you use the more modern async style (async/await in most languages) you get proper exception propagation even then.
On the opposite end of the spectrum, yes, as they pointed out you can just return all of the err's up the stack.
I wasn't saying that Go's approach solves this, just that it's not a problem unique to Go.
And in the case of Go it's painfully obvious that you're ignoring all of those errors whereas in other languages you can't always tell, visually, that they're being ignored because of the magic of exceptions.
>There isn't a meaningful difference in cumbersomeness between having two try-catch blocks and two if-err blocks. There is a meaningful difference in cumbersomeness between what Go has today and "try foo(try bar())". Which is why it's so unfortunate that the community killed the try proposal.
Happy path is flat. Control flow is obvious and simple. I have not much more to add.
>People say this, but in practice any code search reveals that "if err != nil { return err }" is everywhere. I believe that many Go projects aspire to annotate all errors, but much fewer actually do.
>Ironically, the nice thing about exceptions, as well as Rust error chaining crates, is that they do this automatically, so in practice programs written in languages with those features tend to have better error diagnostics than Go programs do.
Rust is a different ball game. Rust does not try to be simple. It comes at its own costs. (I like Rust too.)
>Computers are better at doing things consistently than humans are.
These empty platitudes come up frequently when debating language decisions online. But, it's so meaningless in so many dimensions. I mean, we could also 'use the computer' by adding C macros on top of Go and use them to reduce repetitiveness, but I don't think many people will applaud you for it. Simply applying computer code to solve a problem does not constitute good design.
Go's proof is in the pudding. It's been extremely reliable for me in real world applications.
is the happy path, but it's been hidden among lines of noise that do nothing more than return to the callers who know what to do. Generating this using cpp or m4 would suck, but it's still better than not generating it due to wasted effort (especially re-reading) and mistakes.
How often do you actually need to handle those errors differently? In my experience, it is vastly more likely that a function which can throw errors in Java looks like this:
FWIW when debugging i prefer the second style if for no other reason than that i can place a breakpoint in doOtherStuff while skipping doStuff. Also reading it, it is more obvious that the code calls both doStuff and doOtherStuff (though with just two calls it isn't a big different, imagine having a 2-3 more calls in there).
(also why debuggers still insist on line-based breakpoints is beyond me, why can't i right click at a call and put breakpoint at the call itself instead of the line where the call lies on?)
After working with Go for a while I actually find code with error handling easier to read because it is more clear to me what it happening. I look for the error handling as an indicator that the code can actually fail vs. code that cannot fail.
And maybe it's because I've developed a very consistent pattern, and when I find that there are too many errors to handle in a function it helps me realize the function is probably doing too much and needs to be split into multiple functions.
IMO it is all about training your brain and being accepting enough to work in the dialect of the land, as opposed to demanding to speak English when living in France. :-)
I like your points about diagnostics. I definitely feel this—it seems like one of Go’s weakest points. That said, Go is still one of the best tools available for building software today.
I was one of the many who argued strenuously that they not add try(). I spent an entire day writing up my reasons who I posted to the ticket on GitHub.
Bottom line, try() had too many flaws — especially related to consistency with the rest of Go — to be an appropriate addition to the Go language.
Adding try() to Go would be like adding a disco ball to the Sistine Chapel. Sure, disco balls have their place, but some places are just not appropriate for disco balls.
> Your 'happy path' must be surrounded by try blocks.
Not if you're doing it right, which means bubbling up (read: adding the exceptions to the "throws" clause) exceptions that you can't handle _then and there_. You leave the "real" exception handling to the code that's closest to the end user and can actually handle the error in a meaningful way.
For example, in this pattern when you implement the business logic in a REST service, generally there's no catching at all, instead the exceptions are declared as rethrown and way up the stack you would have a global catch all that serializes the errors into JSON and sends them to the user. Simple. In Go you're forced to do "if err{}" checks everywhere, particularly in your endpoint's business logic. It's actually _way less_ verbose and burdensome in Java if you do exceptions the right way.
> Go doesn't even really force you to check your errors, it just makes it harder to accidentally not check them. Like:
I dislike it because it does the opposite, it makes it too easy to accidentally continue execution when there is an error:
doThing(); // Error
doOtherThing();
Which isn't possible with Exceptions.
The only thing that would signal that error handling is missing is the absence of boilerplate to handle it, which is an ugly UX for a language to rely on esp given there's no indication from scanning code that all methods that return errors are handled.
The nice thing about a statically typed language having standard golint and gofmt is that code is generally self documenting. So I wouldn't ever type that function call without seeing the function definition (in my editor or wherever else I found the API reference).
But I agree, the fact that Go allows this is bad, imo. It would be better if you had to explicitly suppress errors, even with something like this "_ = doThing()" just to make it harder to miss.
I don't know go, so I'm confused here. If nothing failed, the error is just made silent? The program will move to doOtherThing, as if nothing failed, and everything will move forward?
The error was just ignored. if doThing() had some side effect that doOtherThing() depended on then you will never know why doOtherThing() isn't working the way you expect it to be.
Ya that seems pretty unsafe to me. I'm not sure then why others suggest the Go error handling makes things safer. Silent failures have always been some of the most impacting issues in the systems I've maintained. They cause slow corruption and they take a long time to be found, at that point, the damage is done and hard to revert.
> Go doesn't even really force you to check your errors, it just makes it harder to accidentally not check them.
Quite the opposite. You have to explicitly ignore exceptions, whereas it’s easy to accidentally miss an error in Go. For example, how many times have you seen this?
To be fair, reporting that cleanup also failed with a different error is inherently complicated, and so many people got the try-finally version wrong that Java finally added try-with-resources and Throwable#addSuppressed.
A case could be made that the syntax is somewhat nicer but you're paying for that by losing proper scoping and typing without typed exceptions.
With the amount of boilerplate in go that is simply about bubbling up error codes, it really doesn't seem that much cleaner than Java exceptions. An if block just as many lines as a catch block but again, you lose the typing.
And the more I think about it, the more I think well written Java is cleaner than well written go simply because the throws keyword leads to strictly less boilerplate. I feel like this stigma is simply about how much bad Java exists (and there's a lot). I can't help but feel like a WebSphere written in Go would be just as ugly as its current Java incarnation.
Actually, I don't think everyone agrees it feels super bad. I personally like having all of my error handling be explicit, painfully explicit even.
>approaches the Java world back with checked exceptions where principle trumped ergonomics.
I also have to disagree here. To me, checked exceptions are the worst of both worlds. Here you have additional language features, but nearly the same verbosity. Your 'happy path' must be surrounded by try blocks. Worse, everything that happens in a try block is 'flattened.' Not only does this mean you may need many try blocks, sometimes it can even be difficult to take individual statements and put them in one try block. For example:
Also, exceptions have been overloaded to handle everything, including runtime errors. I think this is more subjective but I strongly dislike it. I do think runtime errors should be possible to handle, just ideally through a separate, more explicit paradigm. Go doesn't even really force you to check your errors, it just makes it harder to accidentally not check them. Like: I think this is excellent. It may lead to complaints about an annoying compiler, but most importantly it leads to fewer mistakes. You can still explicitly tell the compiler to shut up, but it is obvious.Language ergonomics are complicated, but the benefits of Go's approach are hard to deny. Unfortunately, nothing comes without downsides, and it seems like solving the error handling ergonomics issue is a tough one. I think repetitiveness aside, the Go error handling ergonomics are great, and that's exactly why they attempted to reduce repetitiveness. But, in reflection, a lot of that repetitiveness can also be reduced by refactoring your code, so it may not even be quite as bad as it seems.