How does Go get exceptions right? Why, by not having them in the first place.
First, a little history.
Before my time, there was C, and errors were your problem. This was generally okay, because if you owned an 70’s vintage mini computer, you probably had your share of problems. Because C was a single return language, things got a bit complicated when you wanted to know the result of a function that could sometimes go wrong. IO is a perfect example of this, or sockets, but there are also more pernicious cases like converting a string to its integer value. A few idioms grew to handle this problem. For example, if you had a function that would mess around with the contents of a struct, you could pass a pointer to it, and the return code would indicate if the fiddling was successful. There are other idioms, but I’m not a C programmer, and that isn’t the point of this article.
Next came C++, which looked at the error situation and tried to improve it. If you had a function which would do some work, it could return a value or it could throw an exception, which you were then responsible for catching and handling. Bam! Now C++ programmers can signal errors without having to conflate their single return value. Even better, exceptions can be handled anywhere in the call stack. If you don’t know how to handle that exception it’ll bubble up to someone who does. All the nastyness with errno
and threads is solved. Achievement unlocked!
Sorta.
The downside of C++ exceptions is you can’t tell (without the source and the impetus to check) if any function you call may throw an exception. In addition to worrying about resource leaks and destructors, you have to worry about RAII and transactional semantics to ensure your methods are exception safe in case they are somewhere on the call stack when an exception is thrown. In solving one problem, C++ created another.
So the designers of Java sat down, stroked their beards and decided that the problem was not exceptions themselves, but the fact that they could be thrown without notice; hence Java has checked exceptions. You can’t throw an exception inside a method without annotating that method’s signature to indicate you may do so, and you can’t call a method that may throw an exception without wrapping it in code to handle the potential exception. Via the magic of compile time bondage and discipline the error problem is solved, right?
This is about the time I enter the story, the early millennium, circa Java 1.4. I agreed then, as I do now, that the Java way of checked exceptions was more civilised, safer, than the C++ way. I don’t think I was the only one. Because exceptions were now safe, developers started to explore their limits. There were coroutine systems built using exceptions, and at least one XML parsing library I know of used exceptions as a control flow technique. It’s commonplace for established Java webapps to disgorge screenfuls of exceptions, dutifully logged with their call stack, on startup. Java exceptions ceased to be exceptional at all, they became commonplace. They are used from everything from the benign to the catastrophic, differentiating between the severity of exceptions falls to the caller of the function.
If that wasn’t bad enough, not all Java exceptions are checked, subclasses of java.Error
and java.RuntimeException
are unchecked. You don’t need to declare them, just throw them. This probably started out as a good idea, null references and array subscript errors are now simple to implement in the runtime, but at the same time because every exception Java extends java.Exception
any piece of code can catch it, even if it makes little sense to do so, leading to patterns like
catch (e Exception) { // ignore }
So, Java mostly solved the C++ unchecked exception problem, and introduced a whole slew of its own. However I argue Java didn’t solve the actual problem, the problem that C++ didn’t solve either. The problem of how to signal to caller of your function that something went wrong.
Enter Go
Go solves the exception problem by not having exceptions. Instead Go allows functions to return an error
type in addition to a result via its support for multiple return values. By declaring a return value of the interface type error
you indicate to the caller that this method could go wrong. If a function returns a value and an error
, then you can’t assume anything about the value until you’ve inspected the error. The only place that may be acceptable to ignore the value of error is when you don’t care about the other values returned.
Go does have a facility called panic
, and if you squint hard enough, you might imagine that panic is the same as throw
, but you’d be wrong. When you throw and exception you’re making it the caller’s problem
throw new SomeoneElsesProblem();
For example in C++ you might throw an exception when you can’t convert from an enum to its string equivalent, or in Java when parsing a date from a string. In an internet connected world, where every input from a network must be considered hostile, is the failure to parse a string into a date really exceptional? Of course not.
When you panic
in Go, you’re freaking out, it’s not someone elses problem, it’s game over man.
panic("inconceivable")
panic
s are always fatal to your program. In panic
ing you never assume that your caller can solve the problem. Hence panic
is only used in exceptional circumstances, ones where it is not possible for your code, or anyone integrating your code to continue.
The decision to not include exceptions in Go is an example of its simplicity and orthogonality. Using multiple return values and a simple convention, Go solves the problem of letting programmers know when things have gone wrong and reserves panic
for the truly exceptional.
Nice posting but …”panics are always fatal to your program. “Not so: a deferred function may call `recover` and stop the panic. http://golang.org/doc/go_spec.html#Handling_panicsA recognised tactic — as seen in regexp, for example — is to have an exported function handle panics (using recover and returning an error) which may be invoked by the (unexported) functions it calls. Code outside the package doesn’t get to see the panics.
Some ramblings on my experiences with panics so far:Don’t be too eager to panic/defer/recover in your code. Return an error. Especially if you’re writing a server with lots of goroutines, where an error in one goroutine doesn’t mean that you want to kill all your users’ sessions.If there is an error condition that really should never, ever happen (like a default in switch or unreachable code), that’s a good place to panic. You might end up nuking your entire server, but you’ll notice the problem sooner rather than later.If you don’t understand how a function can fail, don’t ignore the error. Panic instead. Close is a good example: we’ve caught file descriptor handling bugs in other code by panicking on EBADF from Closes that should have succeeded. This goes for almost any function that eventually calls a system call.Errors returned from some C libraries (especially when dealing with OS/hardware stuff) sometimes means that something is seriously wrong and keeping your process around is probably going to do more harm than good. Now is a good time to panic. Don’t try to recover this panic — think of it is a runtime panic at the same level as a nil pointer dereference.Related to hardware code: if you get a second error while trying to clean up resources after a first error, panic. Your process or its environment is probably broken. Don’t go down the []error road.You can build frameworks where client session goroutines can panic and a defer/recover will rescue the situation, but you end up with the problem of distinguishing runtime panics, C-library-or-hardware-something-is-very-wrong panics and panics-that-should-have-been-errors panics. You can do all kinds of interface checks and other horrible things with value returned from recover, but it’s a world of pain.As far as possible, standard library code that deals with input from the outside world (e.g., gob) shouldn’t panic unless there’s a serious problem in the runtime: it breaks goroutine-per-client-session servers that don’t defer/recover (which is nice to avoid if you don’t want to have to distinguish the various classes of panic). Be liberal and robust in what you accept (i.e. random bytes shouldn’t blow up gob), and return an error if something isn’t right.
You should take a look at how Common Lisp handles exceptions; it uses conditions and restarts which are far more powerful and more sane than exceptions and the primitive way that Go simply returns errors: http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-rest…
Well said, Dave. Here’s a write-up the guys behind Bitsquid just put out that is a bit similar: http://bitsquid.blogspot.com/2012/01/sensible-error-handling-part-1.html(you'll have to scroll up, the link points at the comments for some reason).Anyway, common thought these days definitely seems to be that you either report the error or crash/panic. In practice, especially in Go, this is very productive for the programmer because a huge mental weight is lifted.
Lua does exceptions in a similar manner, and it’s a pretty nice model.
Thank you to all those who have commented.@chris – When I wrote “panic is always fatal”, I had intended the next sentence to qualify what I meant by fatal. You panic without expectation that something will catch that panic. While it is possible to recover from a panic, the practice is hardly common place, and I don’t think it should be the first tool a Go programmer should reach for. In support of this argument, I’m pretty sure that the testing framework uses a recover to try to cleanly recover from test failures. The method under Test is free to blow up, including the quite common panic(“unreachable”) and the test framework will capture that panic. In this way it is as if the test harness was an external program which executed your test code. Using recover allows the test framework to be embedded in the same process as it is testing while achieving a moderate degree of separation from the code being tested.
Thanks for the article. What are your thoughts on whether having errors as regular return values makes for an unsafe error mechanism? Consider Go’s hello-world program (minus boilerplate):fmt.Println(“Hello, 世界”)If I execute this program and it exits with status 0, did everything go smoothly? I am, of course, referring to the fact that Println could return an error. So the answer is no. Yet this is how everyone would write this program.It seems that the current Go implementation makes errors too easy to ignore. Even Java unchecked exceptions are not that easy to ignore, because the make your program crash with a stack trace. Go, on the other hand, happily continues execution, potentially with corrupted internal state, and probably fails elsewhere. How do you find out where the first error happened?Some time ago I proposed “Panicky Return Values” on the Go mailing list. It was met with much skepticism. In the end I wasn’t satisfied with the solution myself. I now have another idea, but I’m not yet ready to post it. Namely, what if we declared error in the return tuple this way?func Println(a …interface{}) (n int, !err os.Error)Notice the ! in front err. This would tell the compiler that if the caller “ignores” this return value, panic! By “ignore” I mean not explicitly assigned to anything, not even the blank variable. Ignoring an error should be explicit, so that a reader knows what is happening:_ = fmt.Println(“Hello, 世界”)This is more than enough to tell the reader that the author intentionally ignores the error. If, however, an error is ignored by mistake a better course of action is to stop the program as early as possible, i.e. panic.
@yegorjbanov, thank you for your comment. You raise two related questions, which I’ll attempt to respond too.1st, ignoring fmt.Println()’s potential error. I tried to address that in the first paragraph of the “Enter Go” section where I said “the only place it may be appropriate to ignore an error is when you don’t care about the value of the response”. Consider a function f() (i int, err error), you can ignore checking the err return value, only if you also don’t care about the value of i. This isn’t a hard and fast rule, there will always be exceptions, but applying that to fmt.Println(), I believe most of the common cases that would lead to Println returning an error would most likely lead to the program terminating regardless of the result being checked or not. 2st, regarding making errors to easy to ignore. Honestly, this isn’t a programming language problem, this is a human problem. I think Go strikes a good balance whereby the use of _ to quash error values is a good signpost for others reviewing the code. An overwhelming number of problems reported to the go-nuts mailing list were quickly identified, and resolved, by commenters noticing the telltale _.Finally I have no comment on your proposed Panicky Errors save to suggest that this corner of the web won’t receive as much attention as the go-nuts mailing list. I would be glad to discuss it there.