Go error handling
code.ohloh.net
code.ohloh.net
If any of these calls to panic() are inside libraries, by using that library, you will put yourself into a position where your application will exit without giving you any recourse to fix it.
I've seen too many C libraries doing this too - "mmh - I can't handle this right now - let me just call exit()" (last time this happened was inside a commerical library for reading excel files. If it was confronted with a file that was corrupt in some way, the library would call exit(), terminating my application server in the process. Thanks)
With exceptions, a library can just throw but I as a consumer of the library at least get a chance to handle the error somehow and who knows - I might be able to find some way to still proceed and not crash.
If I don't handle that exception, oh well, then I crash anyways, but that's not different from the library calling exit or panic.
With exceptions, I also never risk that I (or a library I use) forgets to check for an exit code and proceeds to corrupt data (or crash way later at a seemingly unrelated point).
When I fail to catch an exception, I my application dies.
When I fail to check an error code, my application might possibly corrupt data.
Just because implementing exceptions in compiled languages is difficult and messy (C++) and because exceptions are slow doesn't mean we shouldn't be using them. It means that we have to improve our language and OSes in order for them to work without hacks and quickly enough because, honestly, the value they provide to me is big enough to warrant a bit of research.
Panic is throw. Recover is an isomorphism for catch.
For the most part. You have to explicitly ignore error codes in go. (Not that you can't handle them incorrectly.)
I'm not sure I udnerstand what you mean. I can write: fmt.Fprint(os.Stdout, "Something\n")
This returns an error, which I silently discard. Did you mean something else?
By itself isn't a valid statement. You try to compile that and you'll get an error saying unused return value. So you'll try:
err := fmt.Fprint(os.Stdout, "Something\n")
and you could ignore err, but then you'll get a compiler saying err is defined but never used. So you can be like NAH I'm just trying to muck stuff up so I"m going to do this.
_ = fmt.Fprint(os.Stdout, "Something\n")
Which is explicitly discarding the error much like:
try{ fmt.Fprint(os.Stdout, "Something\n") }catch(Throwable t){}
http://play.golang.org/p/yreUbxx5Vv
So it appears that there is a loophole for things that can error and purely used for side effects.
For simply dropping all return values on the ground, there are linters that will complain for you: https://github.com/kisielk/errcheck
j := m["root"]
or
j, ok := m["root"]
Again, this tangential to the subject at hand. But this is the main reason I qualify that statement.
There are linters which will tell you when you forget to check an error code.
You say that unhandled errors can cause data corruption, but what about improperly cleaning up resources? That happens all the time... you have your data partially written to disk and something throws an exception, if you don't have your cleanup code in a try/finally, then bam, corrupt data. That's a lot more likely with the mindset of generally just letting exceptions fly.
I thought it was officially only discouraged across module boundaries -- e.g., library code shouldn't let panics "escape".
What about it? Are you saying that error return values somehow force programmers to properly cleanup resources?
>> That happens all the time...
How are you quantifying that? I write code in exception bearing languages all the time and never have this problem. Why would you begin a partial write and then call some code that would likely produce errors or exceptions? That sounds like a recipe for corrupt data no matter the style of error catching. A better way would be to use a method of writing that is basically atomic.
A lot of your arguments in this thread sound a bit contrived.
Sorry, what I meant by "happens all the time" is that there are often cases where you need cleanup code that gets called if the function doesn't exit cleanly.
There's very often no atomic way to perform a series of actions. Please keep in mind, I was being very vague on purpose. There is almost always a list of actions that you want to either all succeed, or else you need to do some cleanup if later ones fail.
In code with exceptions, you don't always know what can throw. Unless you're using java code that has been written with strict checked exceptions, you have to assume any external function you call can throw. Even if it doesn't now, it might when Bob from the next team over decides that now it should.
I really don't think it's at all contrived. I'm not saying you can't write this code with exceptions, I'm saying it's harder than it needs to be, and it's at least as verbose as Go, if you're doing all the right things with your try/catch/finally's.
Multithreaded environments make exception handling next to impossible to do correctly. The stack trace only shows a single thread which is often useless without more context. Exceptions are per-thread, so handling has to be per-thread.
Exceptions make the control flow of your program completely obscure. Any line that calls out to another function can cause this function to exit. If you're not wrapping every function call in a try/finally, you're probably doing it wrong (at which point it's even more verbose than go's error returns).
Errors in Go aren't special. They're just values returned from a function. You have the whole language at your disposal to deal with them.
I can write this in Go, and it's perfectly safe:
f, err := os.Open(filename)
if err != nil {
return fmt.Errorf("Failed to open config file: %s", err)
}
err = callFunctionThatMightErrorOut()
if err != nil {
// handle err
}
f.Close()
I know that my code will ALWAYS close that file handle. To do the same thing in a language with exceptions, you'd have to do something like this: File f
try {
try {
f = os.Open(filename)
}
catch(Exception e) {
throw new Exception(string.Format("Failed to open config file: %s", e.Message))
}
try {
callFunctionThatMightErrorOut()
}
catch(Exception e) {
// handle error
}
}
finally {
if f != nil {
f.Close()
}
}
Now who's verbose?The reason people think Go exception handling is verbose is because they aren't really handling errors in their own code. Go makes it easy to handle errors in the right way.
That's what grandparent post should have been using.
f, err := os.Open(filename)
if err != nil {
return fmt.Errorf("Failed to open config file: %s", err)
}
defer f.Close() // This is going to be always executed when leaving this function
err = callFunctionThatMightErrorOut()
...
When writing C++, I wish I had defer over RAII. In C++, when interfacing with lower levels or C code, you need to first wrap everything in a class. Just so that you can use unique_ptr or shared_ptr etc. Or worse, write a scoping class. Ugh. Ugly and so many lines written for nothing...In practise, it's not a big problem, because you learn fairly soon when doing something reversible that a defer is needed to reverse it when leaving scope/function. Like:
func DoSomethingInSomeDir() error
{
err := os.Chdir("somedir")
if err != nil { return err }
defer os.Chdir("..") // always done even if there's panic
... do the something that can fail
return nil
}
Granted, using chdir in the first place is rather evil in anything but in a simple utility. And yes, chdir("..") doesn't necessarily bring us back to the starting position. And it itself can have error, which should be handled in real code. But this is a quick example. :-)But I didn't need to write a ChdirToSomewhereAndBack wrapper class either. Control flow is completely obvious. All potential bugs are in plain sight, not hidden in some wrapper.
If you saw this code for the first time ever, you'd have absolutely no surprises or need to browse code in some class.
If you needed to do this chdir thing often, you could simply wrap it in a function:
func DoSomethingInANamedDir(dirName string, fn() error) error
{
err := os.Chdir(dirName)
if err != nil { return err }
defer os.Chdir("..") // always done even if there's panic
return fn(); // do the something that can fail
}
then just: DoSomethingingInANamedDir("something",
func() err { ... something that can fail ... })
Of course, as someone who sees this for the first time needs to look inside DoSomethingingInANamedDir. Such is the price of wrappers.That's not great. Static guarantees are worlds better than "I can manually look at it, and manually grep for places I need to look".
"But I didn't need to write a ChdirToSomewhereAndBack wrapper class either."
That's not exactly hard, and if you're doing something frequently then writing something to abstract it away is the right approach. It's true that many languages leave defining that wrapper class ugly and push it far away from the site of use in one-offs.
"All potential bugs are in plain sight, not hidden in some wrapper."
That would be one of the upsides, yes. The related downsides are that there is more room for bugs because you have to get it right every time instead of just once, and bugs might be hidden in plain sight when there is too much clutter.
"If you needed to do this chdir thing often, you could simply wrap it in a function:"
That seems isomorphic to the RAII wrapper class, with slightly more syntactic cruft.
Grepping or looking is pretty much what you need to do also in C++, if you happen to have the bug in the wrapper. There's no static guarantee that can figure out if there's no chdir back to original location.
I write C++ for living. About a year ago, I wrote one pet project in Go. 30k lines, complicated concurrency, two different TCP servers, thousands of simultaneous goroutines that send messages over channels to each other. I was shocked how good experience it has been from the start when it comes to error handling and general reliability. Despite being my first and only project in Go, the project just had pretty much no bugs.
We've used this project daily by handful of people for over half a year and so far just a few minor bugs have been reported.
I've never achieved that low initial defect count for similar size and complexity C++ projects.
Here's what you get if you have some magic RAII file-closer object:
try {
f = os.Open(filename)
}
catch(Exception e) {
throw new Exception(string.Format("Failed to open config file: %s", e.Message))
}
try {
callFunctionThatMightErrorOut()
}
catch(Exception e) {
// handle error
}
Still not great.No, it doesn't; wrapping each function call that might throw an exception in its own try/catch is recognized as an antipattern in all of those languages.
Most likely, you'd want to do one try block, with possibly multiple catch blocks for more specific exceptions (but only if you can do something useful to handle those exceptions), although, really, if you are reading a configuration file and the function you are writing can't do anything meaningful with errors but rethrow them, you probably want something more like (and this would be a case for catching generic Exception):
try {
f = os.Open(filename)
callFunctionThatMightErrorOut()
.
.
.
}
catch(Exception e) {
throw new FailedToReadConfigFileException(e)
}The caller knows that the function they called failed, and has as much detail from the underlying error as it makes sense to include in the special case Exception constructed from it.
> They both just cause the same FailedToReadConfigFileException
No, they cause the same class of exception. The data in that Exception can vary as much as necessary based on the data in the underlying exceptions thrown.
Yes, if you actually have a need to differentiate, you may need additional try/catches, but that's usually a violation of encapsulation that exposes internal implementation details of a method to calling code, which defeats much of the purpose of OO, or even merely structured, programming.
using(File f = os.Open(filename)) {
callFunctionThatMightErrorOut()
}
Thanks to exceptions, `using` doesn't have to access the return value from `os.Open`, it just has to handle the exception that it (or any code inside the `using` block) might raise. Also thanks to exceptions, using can automatically cleanup `File`. Without exceptions, this simple pattern would not work very well at all.When using go I often get the feeling of doing something wrong. Because I know I'm supposed to "handle errors" but all I really want is the program to blow up and hand me a stack trace. This gives me the worst of both worlds, I have to manually trigger something that approaches the unhandled exception behavior of other languages while retaining the verbosity of explicit errors returns. Now it seems like I'm not the only person struggling with this.
Has anyone come up with a sane approach to that problem?
I write server code the runs for long periods of time, the amount of times I want to blow up and get a stack trace is exceptional low, only when continuing to run is more harmful than having the service completely down.
As for just wanting it to blow up -- just ignore the error handling, it will blow up. Instead of putting the error in a variable, just throw it away using _ ... and the next dependent thing will blow up. Not very elegant, but it will remove all the boilerplate you don't like, and it will crash hard like you do like... alternatively write a crashOnError(err error) function and call that passing in error -- and all it has to do is if (err != nil) { panic(err) }
> Because I know I'm supposed to "handle errors" but
> all I really want is the program to blow up and hand me
> a stack trace . . . Has anyone come up with a sane
> approach to that problem?
Yeah, stop doing that :) Code your error path first, add your program logic afterwards. It's a shift in the way you think when writing programs, but it's a good shift and it makes your code more robust.Error return values (e.g. go) are better for building large systems where stability & predictability are important (that is, cost to maintain).
Exceptions are better for quickly building systems (that is, cost to build).
Exceptions are much better for large systems. You can define standardised behaviour regardless of where an error occurs. You can let unexpected errors 'bubble up' and be managed in a way that preserves the integrity of the system. You have the flexibility to recover from errors and let errors be silenced.
There is unparalleled flexibility with exceptions. Go's approach is frankly arcane.
You are 100% right, it is terrifyingly flexible. There is a reason exceptions as flow control is considered an anti-pattern.
Go's error handling approach is also flow control. You receive an error and have to go and do something different based on that error. With Go it is explicit. With say Java you have the choice of being explicit or implicit. Flexibility is vital when you are working on large applications where the business logic varies wildly throughout the system.
Explicit is way better than implicit by an order of magnitude. It means you can look at any Go code and instantly understand what the control flow will be. You can't do that with languages that have exceptions.
>> At any point in a function, you can suddenly jump...
That's kind of the point. There's a separate path for exceptional behavior, which is what makes reasoning about exception paths easier.
>> It's certainly possible to write correct programs with exceptions, but it's quite hard, and almost no one does it correctly.
You keep saying that, but where's the proof that Golang programs are any more "correct"? How exactly are you quantifying that?
>> Explicit is way better than implicit by an order of magnitude.
That's like saying that having to think about putting one foot in front of the other is better than just walking down the street. It's rather ridiculous to say that.
>> It means you can look at any Go code and instantly understand what the control flow will be.
Not at all because your control of flow is now so concerned with error handling that you can't see the intended "happy path" logic.
f, err := os.Open(filename)
if err != nil {
return fmt.Errorf("failed to open config file: %s", err)
}
// happy path
compare to: File f;
try {
f = File.Open(filename)
}
catch (Exception e) {
throw new Exception(String.Format("failed to open config file: %s", e.Message))
}
// happy path
So, either you write code that looks like this, which is just like the Go code, or you don't, and you're handling your errors in a poor fashion.What most people do, is they would let this function throw FileNotFoundException, and not catch it. There's at least three problems with that:
1. It leaks your implementation, so if later you're saving to memcache, you'll throw some different error, which could break consumers that have written code to handle the FileNotFoundException
2. It lacks context: file not found: foo.yaml ... wtf is foo.yaml and why was this random function trying to open it?
3. You can't tell programmatically what failed. Something threw a filenotfound... but you don't know if it was opening the config file or some other function, so you can't handle it properly.
There's also the fact that now anyone who calls your function needs to wrap it in a try/catch, or let an exception from 2 levels deep bubble up. So you can get FileNotFound: foo.yaml from your Initiate() function, and no one knows what that means.
That's what I mean by "almost no one [handles exceptions] correctly".
Just so. You think something being used in enterprise is a worthy argument?
That doesn't mean Go's "error handling" is any better.
It's probably the worst approach one can pick. (But this has been discussed dozens of times already and Go fans have decided that they don't want to hear it.)
Rust and Swift designers picked a similar approach. Is it possible that three separate designers of languages are so bound by language trends that they decided to make the almost exact same mistakes?
At the "Bank of Golang" though - they've made every customer get strip searched on their way in because of something that happens 1% of the time. No thanks. I'd rather go across the street where they simply have some cameras and security guards watching the crowd for exceptional behavior.
There is slight difference between "the database is on fire" and "a user with user id 42 couldn't be found in the database", don't you think?
Go handles neither of these situations gracefully.
void SomeHighLevelMethod() {
...
catch (const DatabaseOnFireException& e) {
// TODO log something here
// TODO do something here
}
...
}I get the personal preference one way or another, but can you go into more detail about what specific features one loses with Go's approach?
func (t *Thing) Delete() error {
count, err := db.Delete(t)
if err != nil {
return err
}
if count != 1 {
return errors.New("delete Thing didn't delete 1 row? count: " + string(count))
}
return nil
}And exceptions are not the only thing that do this automatically for you, so this is just flat out horrible design decision.
I deal with systems that contain millions* of lines of C++. Exceptions are killing us. How much larger do we need to get until I can expect to see the advantages?
* I haven't actually counted those lines or even wc -l'd them.
It looks much worse than exceptions. (Except C++ and Java exceptions, as both are exception anti-patterns in different ways.)
1) fix it locally 2) terminate 3) ignore 4) delegate decision to caller
(1) is rarely an option because failure conditions are usually beyond the control of the low-level code performing the operation. That's because error conditions usually occur at the edges of an application, typically I/O of some sort, and typically are the implementation of a higher-level semantic action. If you're trying to open a file or connect to an address, what can you reasonably do if the file is missing or the network is unavailable?
(2) is not an option for systems beyond a trivial size, unless the system is composed out of little executables. There's an argument for going this route in an actor system, but most Go apps are not actor systems.
(3) is usually only an option for throwaway programs - they're probably better off terminating, in case they do something bad after preconditions have been violated.
Which leaves (4) - delegation to the caller. This is the most composable option, since it lets the composer of the components control the error strategy. In its most flexible form, this looks something like Lisp conditions; it's easy enough to emulate with a stack of error handler callbacks, but the benefits only come when the handling system is consistently used everywhere. The more common version is to unwinds the stack until someone is able to deal with the problem.
Whether you unwind explicitly with checks to an 'err' return code, or implicitly with panic, or with longjmp, or with exceptions in a language with that feature, doesn't really matter. You're still doing the same thing, semantically and with code flow.
Which error handling strategy is more error prone?
The most manual one - the one that makes it easiest to drop the error on the floor, or otherwise molest it on its path from cause to handler - would be my bet.
Java's pathology here is statically checked exceptions; this encourages molestation of the error on its path, since the error value potentially needs to be of a different type at different points in the code flow. This risks obscuring the cause of the error, since errors are a function of implementation - but Java encourages expressing errors in terms of interface.
C++'s pathology is that it allows code flow through user provided code that is ill suited to throwing exceptions. The added complexity spawns whole books worth of material on how to write C++ code in an exception safe manner.
Good exception handling style in languages with sane exceptions normally means almost never catching exceptions except for high level orchestration routines; reacting to unwinding by cleaning up resources; and generally taking a transactional approach to operations.
I believe that good error handling with explicit error codes doesn't do anything semantically different in response to an error - it just does it all manually, i.e. in an error-prone fashion. And it's my biggest problem with Go.
I agree. Except that is really really really hard. How does your orchestration routine know to close the file handle that was opened 6 levels deep in the code? How does it know to return 404 not found to the request that was being handled 4 levels deep? It can't, and if it does, then you're breaking encapsulation.
The reality of the situation is that the only place where you have the knowledge to clean up after a function is inside that function. I suppose in theory you could write a whole crapload more code to create auto-clean-up objects and transactional frameworks.... and that'll take a lot longer and be a lot buggier, and be a lot less flexible than just handling the error near to where it was produced with boring, straight-forward code, which is how it works in Go.
It doesn't need to. Which is why "never catching exceptions except for high-level orchestration routines" is a separate point from "reacting to unwinding by cleaning up resources".
What is being said is that, in good exception handling style, Orchestration routines use try/except (they might use "finally" if they also have local resources to clean up), and lower-level routines use try/finally without "except".
> and that'll take a lot longer and be a lot buggier, and be a lot less flexible than just handling the error near to where it was produced with boring, straight-forward code, which is how it works in Go.
With Go, you pretty much do the same thing -- lower level code cleans up its own resources (with defer in place of finally) and will propagates errors up in the same circumstances that code would in other languages (with explicitly error returns rather than implicit propagation of exceptions), and higher level orchestration routines are mostly where you do something more with the error returns.
It probably makes the most common mistakes in error handling in Go different than with languages where the preferred method is more "traditional" exceptions, because the easiest mistakes to make are different.
When I first started programming C I also tended to just exit the program on getting a bad return code, because I'd never been taught how to properly handle errors - I'd always just replied on exceptions.
Only later did I start to get bitten, and take proper care to handle errors sensibly and explicitly.
As "go" is such a new language I wouldn't be surprised if a similar thing has happened to new programmers migrating over to it.
func LowLevelCode() (int, error) {
// rest of body...
if /* error condition */ {
return 0, errors.New("LowLevelCode error: " + /* error description */)
}
}
func MiddleWare() (interface{}, error) {
// body of code...
nCount, err := LowLevelCode()
if err != nil {
return nil, errors.New("MiddleWare error: " + err.Error())
}
}
func ApplicationCode() {
// Do cool stuff...
obj, err := MiddleWare()
if err != nil {
log.Println(err) // Prints "MiddleWare error: LowLevelCode error: " + ...
// Handle on a high level, retry connections, etc.
}
}
I'm hoping someone comes in and corrects me with a more elegant way of "building up your own stack trace" without having to do this manually and without panic.Basically you just call a function every time you would return an error, and it automatically records the file and line number and lets you add on some context to the message, plus it gives you the option to mask the type of the error (to hide implementation details from your callers).
I wouldn't use errgo right now, it's still under heavy development, but it's similar to a lot of other error handling libraries out there.
Or is it just a random code search posted on HN?
if err != nil{
log.Printf("This specific operation %v went
wrong because input %v was %v or was expected to be %v but
wasn't", operationName, inputName, actualInput,
expectedInput)
}
Then reading the error messages gives a pretty good idea of what's supposed to be happening at that point in the code.Which is also my opinion.
> most often they either exit on exception (which is like
> not catching an exception)
Not really. Exiting on error is an explicit decision that you make, hopefully only in your top-level (func main) scoping block. Letting an exception bubble up to that level without catching it is invisible. > or pass the error up the call stack (the default
> behaviour for an uncaught exception)
The crucial difference, which the Go team takes great pains to elucidate and fans of exceptions never seem to acknowledge, is that by treating the error as a first-order value, you make its propagation up the call stack explicit, in the normal i.e. visible code path. That makes reasoning about your error path a lot easier, and makes producing code that handles errors correctly a lot simpler.The "Eureka" of error handling in Go isn't a massive thing. It's just the reification of the small and important understanding that errors are values, just like anything else.
The thing that some Go fans miss is that it's actually pretty rare (in my own experience, but I've seen a lot of people echo the same thing) to have to deal with an exception. Usually an exception means that the show is over -- at best you'll report it to the user and continue running your main loop if your application is interactive. Or you may retry the operation a certain number of times.
But most of the time you just want to stop the application, or let some upper level deal with it, and those are exactly the default behaviors of exceptions.
With un-checked exception handling code, all I have to do is look for "catches" in the call stack to my function. That code stands out and it's easy to find. With Golang, I have to spend way more energy analyzing every caller in the stack because they all touch my error.
> So, your argument is that - because you actually see
> some code that simply propagates an error, it's easier
> to reason about where the error ends up being handled?
Not just where the error gets handled, but how it flows through the execution path of your code -- but, yes. > That's like saying you'd rather search for a needle in
> a haystack than a needle on a platter.
I'm sorry, but I don't see how this follows at all. > With Golang, I have to spend way more energy analyzing
> every caller in the stack because they all touch my
> error.
Spending time thinking about the error path for all of your code is time well spent, because it's necessary if your code is going to function correctly. Deferring consideration of failure results in dangerous (at best) and/or broken (at worst) systems. I hope this is noncontroversial.In the end I think the problem is that there are two separate classes of programs. The first is where crash on exception is tolerable and even a desired property (or blindly and optimistically continuing). The second is always deal with exceptions and any error no matter how rare and unlikely they are, and attempt to recover and deal with them properly. No one language and standard library can really please both sides.
while x != y
x = ...
y = ...
end while
-- (A)
At point (A) we know that x == y. This allows us to reason about what happens next in the program. Having a break statement within the loop means that we now have a more complex condition to consider at the end of the loop. while x != y
x = ...
if x == 0: break
y = ...
end while
-- (B)
At point (B) we know that x == y or x == 0.Once we introduce exceptions, we can end up outside the loop without knowing anything about the state of x and y (unless we examine a lot more code, possibly code quite remote from this little loop we are working on).
go's requirement to laboriously check and handle every function return may make it easier to reason about x and y in our little example, but it comes at the cost of extra boilerplate error checking everywhere. This too hurts our ability to reason about the code we are writing by increasing verbosity, so I'm not sure that go's approach will work out much better than some form of exceptions mechanism.
However, I welcome this approach to language design, keeping the language as simple as possible, but no simpler. Even if go error handling turns out in practice to be roughly as good as C++'s approach, it will be a big win for go because go ends up smaller, simpler, and easier to learn and master. It's difficult to assemble a team of C++ programmers to work on complex systems because it is so hard to find more than a handful of real C++ experts at a time. I feel safer recommending the use of go to a typical programming team than C++.
func mustBeNil(err error) {
if err != nil {
panic(err)
// or log.Fatal(err)
}
}
or func mustBeNil(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", err, msg)
}
}
;Edit: bracketsYou panic when a program is expected to crash because of irrecoverable corruption. Only.
> You panic when a program is expected to crash because of
> irrecoverable corruption.
Or programmer error.