Public
I've gotten a fair amount of mail about a recent blog post titled "Why I'm not leaving Python for Go" [1], in which the author says that Go is great except that "errors are handled in return values". I thought it would help to write a little about why this is.
In Go, the established convention is that functions return errors; they don't panic. If a file isn't found, os.Open returns an error; it doesn't panic. If you write to a broken network connection, the net.Conn's Write method returns an error; it doesn't panic. These conditions are expected in those kinds of programs. The operation is likely to fail, and you knew that going in, because the API designer made it clear by including an error result.
On the other hand, there are some operations that are incredibly unlikely to fail, and the context is such that there is no way to signal the failure, no way to proceed. These are what panic is for. The canonical example is that if a program evaluates x[j] but j is out of range, the code panics. An unexpected panic like this is a serious bug in the program and by default kills it. Unfortunately, this makes it difficult to write robust, defensive servers that can, for example, cope with the occasional buggy HTTP request handler while keeping the rest of the server up and running. To address that, we introduced recover, which allows a goroutine to recover from a panic some number of call frames below. However, the cost of a panic is the loss of at least one call frame from the stack. We did this intentionally. To quote from the original mail: "This proposal differs from the usual model of exceptions as a control structure, but that is a deliberate decision. We don't want to encourage the conflation of errors and exceptions that occur in languages such as Java."
The post I mentioned at the start asks "why is an array out of bounds any more cause for panic than a bad format string or a broken connection?" The answer is that there is no in-band way to report the error during the evaluation of x[j], while there is an in-band way to report an error in a bad format string or a broken connection. (The format string decision is interesting by itself but orthogonal to this discussion.)
The rule is simple: if your function is in any way likely to fail, it should return an error. When I'm calling some other package, if it is well written I don't have to worry about panics, except for, well, truly exceptional conditions, things I shouldn't be expected to handle.
One thing you have to keep in mind is that the target for Go is programming in the large. We like to keep programs concise, but not at an increased maintenance cost for big programs worked on by large numbers of programmers. The seductive thing about exception-based error handling is that it works great for tiny examples. But diving into a large code base and having to worry about whether every single line might, in ordinary operation, trigger an exception worth handling is a significant drag on productivity and engineer time. I have had this problem myself finding my way around large Python programs. The error returns used by Go are admittedly inconvenient to callers, but they also make the possibility of the error explicit both in the program and in the type system. While simple programs might want to just print an error and exit in all cases, it is common for more sophisticated programs to react differently depending on where the error came from, in which case the try + catch approach is actually more verbose than explicit error results. It is true that your 10-line Python program is probably more verbose in Go. Go's primary target, however, is not 10-line programs.
Raymond Chen's articles are the best exposition I've seen about the pitfalls of trying to do error handling with exceptions:
http://blogs.msdn.com/b/oldnewthing/archive/2004/04/22/118161.aspx
http://blogs.msdn.com/b/oldnewthing/archive/2005/01/14/352949.aspx
Suffice it to say that the Go developers agree: error is so important we made it a built-in type.
Russ
P.S. Occasionally you see panic and recover used as a kind of non-local goto, similar to longjmp and setjmp in C. This is fine too, but only as an internal detail of your package. If callers need to know, you're still doing it wrong.
[1] http://uberpython.wordpress.com/2012/09/23/why-im-not-leaving-python-for-go/
In Go, the established convention is that functions return errors; they don't panic. If a file isn't found, os.Open returns an error; it doesn't panic. If you write to a broken network connection, the net.Conn's Write method returns an error; it doesn't panic. These conditions are expected in those kinds of programs. The operation is likely to fail, and you knew that going in, because the API designer made it clear by including an error result.
On the other hand, there are some operations that are incredibly unlikely to fail, and the context is such that there is no way to signal the failure, no way to proceed. These are what panic is for. The canonical example is that if a program evaluates x[j] but j is out of range, the code panics. An unexpected panic like this is a serious bug in the program and by default kills it. Unfortunately, this makes it difficult to write robust, defensive servers that can, for example, cope with the occasional buggy HTTP request handler while keeping the rest of the server up and running. To address that, we introduced recover, which allows a goroutine to recover from a panic some number of call frames below. However, the cost of a panic is the loss of at least one call frame from the stack. We did this intentionally. To quote from the original mail: "This proposal differs from the usual model of exceptions as a control structure, but that is a deliberate decision. We don't want to encourage the conflation of errors and exceptions that occur in languages such as Java."
The post I mentioned at the start asks "why is an array out of bounds any more cause for panic than a bad format string or a broken connection?" The answer is that there is no in-band way to report the error during the evaluation of x[j], while there is an in-band way to report an error in a bad format string or a broken connection. (The format string decision is interesting by itself but orthogonal to this discussion.)
The rule is simple: if your function is in any way likely to fail, it should return an error. When I'm calling some other package, if it is well written I don't have to worry about panics, except for, well, truly exceptional conditions, things I shouldn't be expected to handle.
One thing you have to keep in mind is that the target for Go is programming in the large. We like to keep programs concise, but not at an increased maintenance cost for big programs worked on by large numbers of programmers. The seductive thing about exception-based error handling is that it works great for tiny examples. But diving into a large code base and having to worry about whether every single line might, in ordinary operation, trigger an exception worth handling is a significant drag on productivity and engineer time. I have had this problem myself finding my way around large Python programs. The error returns used by Go are admittedly inconvenient to callers, but they also make the possibility of the error explicit both in the program and in the type system. While simple programs might want to just print an error and exit in all cases, it is common for more sophisticated programs to react differently depending on where the error came from, in which case the try + catch approach is actually more verbose than explicit error results. It is true that your 10-line Python program is probably more verbose in Go. Go's primary target, however, is not 10-line programs.
Raymond Chen's articles are the best exposition I've seen about the pitfalls of trying to do error handling with exceptions:
http://blogs.msdn.com/b/oldnewthing/archive/2004/04/22/118161.aspx
http://blogs.msdn.com/b/oldnewthing/archive/2005/01/14/352949.aspx
Suffice it to say that the Go developers agree: error is so important we made it a built-in type.
Russ
P.S. Occasionally you see panic and recover used as a kind of non-local goto, similar to longjmp and setjmp in C. This is fine too, but only as an internal detail of your package. If callers need to know, you're still doing it wrong.
[1] http://uberpython.wordpress.com/2012/09/23/why-im-not-leaving-python-for-go/
View 66 previous comments
Many will disagree with my, but... this is a place I use the assignment part of if-else chains.
If need be, I 'var' declare the pertinent variables earlier, just so that I can gain the structured error handling.
var (
id int
data string
err error
)
for _, i := range list {
if id, err = i.SendData(); err != nil {
return err
} else if data, err = i.ReceiveData(); err != nil {
return err
} else if value, err := strconv.ParseInt(data, 10, 64); err != nil {
return err
} else if value != 16 {
return errors.New("some error")
}
// apparently you don't need value after the parsing, since
// it's assumed to be '16'
}
// all the set variables will have the last iteration's values
With this, you get a few benefits. Many will say that this is less readable, since you have to read past the [else if] to see what is being assigned. I disagree with that notion, just as I disagree with opponents of gofmt, if there are any.
When enough people use this style, just as with gofmt'd code vs non-gofmt'd code, it makes visual scanning very fast, and so readability is high. Visually, the if/else chain, along with assignment forms and err use in most of them makes me start reading this as an "error handling chain". The unindented lines (the if's) show what's being assigned for later use, and `err != nil` is a subconscious sentinel saying that yes, we are handling the error case in this if block. The `return err` is also a subconscious sentinel that can be noted and quickly scanned past. Because the next important thing is in the next unindented line, it's easy to jump to.
Cases where specialized error handling occurs (checking for a specific kind of error, or checking for the non-error case) are easy to visually spot because the regularity of these chains makes anything unusual "stick out". This also includes places where the error is actually being handled rather than just passed on to the caller.
In the previous "unstructured" form, I have to read the code, consciously, line by line to understand all that it does, and visual scanning has few anchor points. In the structured form, I only have to read the first half of ~1/2 the lines consciously in order to gain the same understanding, and the code is rife with visual anchor points.Oct 2, 2012
+Kevin Gillette readability does become a major issue there if your function calls require lots of parameters, or a formatted string, etc. It also breaks if you need to do three calls that can't error before the erroring call right in the middle of the chain. =)Oct 2, 2012
Yeah, but... best tool for the best job. error handling chains certainly can be detrimental to readability in cases where they're not pertinent, including uses of really long function calls (I find that visually, the `err != nil` part should be at least 1/5th the maximum 'if' line length in a short chain, in order to be optimally readable. It's less important in long chains (10 or more if's).Oct 2, 2012
func somfunc(somearg sometype,....) (result sometype, err error){
for i := range(list) {
var (
id int
data string
value int
)
if id, err = i.SendData(); err != nil {
return
}
if data, err = i.RecieveData(id); err != nil {
return
}
if value, err = strconv(data, 10, 16); err != nil {
return
}
if value != 16 { err = new_error; return }
}
}
And deal with the error in the caller of somefunc.Oct 2, 2012
LabView is an odd language, but it's also an interesting point of comparison here. LabView doesn't have exceptions, instead it's designers encourage a particular pattern of passing errors.
There is something rather like a struct that contains a boolean, an integer and a string. The boolean is the error state: error or no error. The integer is an error number and the string a text description. I'll call that "error_s" for error struct.
If LabView were written like C it's functions would be like:
error_s foo (error_s err, int x, int y)
{
if (err.state = 0) {
... Rest of function ...
}
}
The style encouraged is that each function always takes and error as an input and returns an error.
Code is written like:
err = Set_Temp(err, T, 1);
err = Set_PSU(err, V, 2, addr);
err = Init_Results_File(err, name, path, ext, T, V);
switch (err) {
... error handler...
}
An error can occur in each of those function calls, if that happens then the following functions won't execute. They won't execute because each one is written like foo above.
In my view this gives the best of both world. Errors can be dealt with locally, or they can be left to percolate up the program. Errors in the lowest level modules can easily travel all the way to the top. Error codes allow particular errors to be dealt with locally, but other errors to be left to travel upwards. Boilerplate code is needed at the beginning and end of each function, but not anywhere else. There's no need for language support either, this could be done in C or Go.
It does have its problems though, programmers can ignore the convention or reuse error code in confusing ways. There's no possibility to do the tricks with unwinding that Common Lisp does.Oct 21, 2012
Apparently, it is a matter of taste. FWIW, I am not convinced that exceptions are a bad idea for big programs. My Python code maybe catches 10% of all exceptions that theoretically can occur but they've never occurred – in years. This makes the code tidy and maintainable. In Go, this kind of approach is impossible because the default behaviour is the opposite: Errors pass silently, unless explicitly handled. This „ignore by default“ is the only thing about Go that bothers me.Oct 31, 2016
Add a comment...