Hacker News new | past | comments | ask | show | jobs | submit login
Why Go Gets Exceptions Right (cheney.net)
75 points by motter on Sept 14, 2012 | hide | past | favorite | 81 comments



To be honest I don't think there's any practical difference between this and using `errno` in C. In fact, I suspect Go may have adopted a more complicated solution simply because we're squeamish about using global variables.

At the end of the day this solution doesn't work for "high level" code for the same reason `errno` and return codes aren't adequate - is forces the user to litter their code with explicit checks for exceptional cases. The whole point of exceptions is that they let the user write the code as if everything is peachy, to let the code bail out wherever appropriate, and to pass the buck for fixing it to someone who cares.

(I don't know anything about `panic`/`recover`.)


The practical difference is that there is (partial) enforced checking of errors. If you do not wish to check the error code, you must be explicit.

    f := os.Create("output.txt")
Will give a compile error

    multiple-value os.Create() in single-value context:
and should be written as one of these:

    f, err := os.Create("output.txt")
    f, _ := os.Create("output.txt")
where the second indicates that you are ignoring the returned error.

I say "partial" because if you ignore all the return values you can implicitly ignore an error:

    fmt.Println("Hello")
It is quite common to see:

    if err != nil {
        //handle error
    }
in Go, as their philosophy is that the caller should handle the error, not an unrelated piece of code. Yes, this does sometimes look like "litter", and it doesn't really make for the most impressive code. However, you won't be surprised by it, it's simple to understand, and if something goes wrong, you'll know what, when, and what was done about it. This has been Google's opinion on C++ for a while. http://google-styleguide.googlecode.com/svn/trunk/cppguide.x... lists some pros and cons. Writing code that ignores errors is one way of writing code, I think it's idiomatic Java and Python (please correct me), but that's not the way that was chosen for Go.

I don't really use panic() except to indicate bugs:

    if playerCount == 2{
        go multiPlayer()
    } else if playerCount == 1{
        go singlePlayer()
    } else {
        panic(playerCount)
    }
However they can be useful inside libraries. For example if you call fmt.Printf("%s",s), where s has a method s.String(), will print the result of s.String(). Sound good? But what if s is a nil pointer? Well, you may wish s.String() to handle that and return "A nil s", which is fair enough. However is s.String does not handle this case it may panic(), leaving fmt.Printf to recover() and write "<nil>" instead.

http://golang.org/src/pkg/fmt/print.go#L630


For me it's very common to defer error handling. For instance I'm logging most runtime errors and later I receive a nice report over email. I then click on the links provided and I get nice call-stack traces. I then go and fix those errors.

And while it would have been better to treat those errors in the first place, we do know that's not possible and trying to force the developers only leads to code such as ...

    try {
       ...
    } catch(Exception ex) {}
Checkout this piece on checked exception, also by a Google employee:

http://googletesting.blogspot.ro/2009/09/checked-exceptions-...


This doesn't sit well with me. Who is to say what's catastrophic and what's not? Should I use all libraries in fear because passing a nil pointer or an improperly formatted date may cause it to terminate the program with no recourse? Suddenly every library is a ticking time bomb with a scorched Earth policy for things it doesn't like. How am I to know what's going to cause my valuable program to simply die in the middle of the night? Look at the outdated/incomplete documentation? Pore over the complex code (if it's even available)? Now not only do I need to litter error value checks everywhere, but I also need to preemptively sanitize all input to any library calls I make and pray that I haven't missed something. This is simply trading one problem for another.


That's a potential issue with any language, I believe. In Go you can recover() from what would otherwise be a termination. For example, I have a function that grabs and parses RSS feeds. If my program is fed junk by the remote site, I don't want to terminate, I want to ignore the error for that feed (after logging it). To do this I recover() from any error at the feed level.


Is your opinion set by the article's assertion "panics are always fatal"? Because that is incorrect, there are good mechanisms for handling them.


And to think what they could have done if they'd ever heard of the Exception Monad.


> If you do not wish to check the error code, you must be explicit.

Or you can ignore all the return values, like in your fmt.Println() example.

> I say "partial" because functions that only return 1 value can ignore the return implicitly:

fmt.Println() returns two values, one of which is of the error type. You can't ignore just one of those values, but you can ignore both of them.


Correct. Thank you.


The big problem with errno is that it's a global: there's exactly one of them for your program. As soon as you start using threads you are screwed.

Go aims to make concurrency right. That means you can depend on a global variable for propagating error information.

errno in C is ultimately because C doesn't have multiple return values, and the alternatives (pointer arguments as implicit output) are sucky. Go has multiple return values, and so returning an error is natural.


errno is generally defined to be a macro like: (errno_location()), at least when you use the -pthread option with gcc.

The odd constructoin (the function returning an int which is dereferenced) allows the errno macro to work as an "lvalue", i.e. that you can still say "errno = 0" to clear its value.


errno is thread locale in most modern implementations


This was my assessment as well -- it's just a cleaner way to use errno.

I do think a language got this system right -- conditions and restarts in Common Lisp solve this problem very well. I'm a bit perplexed that I haven't seen this model adopted a bit more widely in the programming world, particularly given how much of a headache exception semantics are.


In "The Design & Evolution of C++" (http://www.stroustrup.com/dne.html), Stroustrup talks about termination versus resumption semantics for exceptions. Resumption was considered for C++, but rejected based on experience in the Cedar/Mesa system. There's a quote from that part of the book on Wikipedia: http://en.wikipedia.org/wiki/Exception_handling#Exception_ha...

Note that I'm not arguing that termination semantics are better - I've never programmed with resumption semantics, so I can't say. But that's why it's not in C++.


Because resumption wasn't actually necessary for such a context inquiry, they removed it and found a significant speed increase in that part of the system.

s/resumption/exceptions/ and you have a secret to performance in Java.

If this is the core reason for termination-only exceptions in C++, Stroustrup made a huge mistake: he presumed that the best exception handling for mature, stable code was the best exception handing for all code. But then he also threw in a lot of features that cripple the language from ever being able to create truly mature, stable code (see http://www.250bpm.com/blog:4). Now the most stable, mature libraries are written in C.

C++ exceptions are then unreachable code, and a properly optimized programmer will never use them.


That's pretty shocking, given Stroustrup's propensity to just dump everything into C++. It's also a bit of a shame it didn't get a chance to see the wider audience C++ would've brought.


People have that impression - that Stroustrup "dumped everything into C++" - but it's demonstrably not true if you read D&E. Every new feature that made it into the language after the standardization point had a working implementation, and people had experience with that implementation. He goes over lots of rejected features.

Also consider what your statement implies: Stroustrup put too many features into C++ (bad), but he did not put feature X into C++ (also bad). You're faulting him for accepting too much, but at the same time, for not accepting one in particular. Although it should really be noted that post-standardization, what made it into C++ was a committee decision, not Stroustrup alone.


Hah, I have a bookmark on the CL approach I've been meaning to get to for months now. I guess I'll have to give it another whirl.

Ack, and your sibling comment is pointing me to a monadic approach... I'm sure "learn about monads" is also languishing on my to-do list somewhere.


Do you have a link to the CL approach?

Edit: A quick search turn up this interesting LtU discussion: http://lambda-the-ultimate.org/node/1544


I'm on a different computer now, but iirc the one I have bookmarked is http://www.gigamonkeys.com/book/beyond-exception-handling-co...

Thanks for your link too.


Very interesting article, thanks for sharing.

I just submitted it here: http://news.ycombinator.com/item?id=4521675


This entire book is a really good into to CL.


There was a great thread in a previous discussion on HN about error handling that had a reply from lkrubner (currently, and likely forever, the top response on that thread) that went into some detail, causing more ensuing discussion. (edit: Ah, and then he links to that same article you linked to!)

http://news.ycombinator.com/item?id=4468849


Do you have any opinion on using Monads for exception handling (like in Haskell)?


It's a way of getting context-specific (but not error-specific) behavior, and when dealing with functions producing a polymorphic monadic value it threads very nicely. That said, the approach in Haskell of fail always taking a string (which doesn't necessarily get stored/used anywhere) has a bit less to recommend it. It's also slightly confused conceptually to put "fail" in the Monad typeclass but that mostly bugs the theorists. The biggest downside, though, is that it can often make the source of errors harder to find - cf. hunting NaNs in floating point arithmetic.


Oh, we just pretend that the monad typeclass does not have "fail" in it. It's only useful for pattern match failures when using the do-syntax.

I was talking more about the (Either Error) monad, which has not much to do with "fail".

(Edited first sentence.)


Ah, yeah, wasn't as aware of those as I should have been. Still suffers from the "where did that NaN come from?" issue, but looks like good stuff. Any idea why it's not used more?


Haskell actually has three (or four) competing exception handling mechanisms nowadays.

If you use the Either error result approach, you can conceivable put a stack trace (or something similarly identifying) in the Error data-type, so you can avoid the "Where did that NaN come from?" issue.

I think we need to see Algebraic data types in more languages, before we will see this approach used more often. (In my opinion algebraic datatypes and the pattern matching they enable rank in the same league as garbage collection in that they are a feature originally invented in and for functional languages, but useful outside as well.)


> Haskell actually has three (or four) competing exception handling mechanisms nowadays.

Yeah, knew about Either in general (see my top-level comment about sums vs. products), just not the monadic bits around it (MonadError and ErrorT, in particular).

> If you use the Either error result approach, you can conceivable put a stack trace (or something similarly identifying) in the Error data-type, so you can avoid the "Where did that NaN come from?" issue.

True - even just making exceptions take a Loc parameter seems like it could be a good step, but that would be basically requiring TemplateHaskell, which would ruffle feathers for sure...

> I think we need to see Algebraic data types in more languages, before we will see this approach used more often. (In my opinion algebraic datatypes and the pattern matching they enable rank in the same league as garbage collection in that they are a feature originally invented in and for functional languages, but useful outside as well.)

I agree wholeheartedly. Lack of easy-to-use algebraic datatypes is my biggest pain-point in languages that lack 'em.


I like them. The only annoyance is that you're forced to wrap your return value in a monad even if the function succeeds. That's type safety for you!


Yes. But that's because the default is non-monadic wrapped and an assumption that it can't go wrong. I'd had to see non-monadic wrapped values to silentry go wrong or raise an exception. (You can do something like Go's panic in pure values. But you are not supposed to do that for recoverable conditions.)


In practice, wrapping a return value in a monad looks exactly like returning a regular value in another language: return x.


Since errno is now thread local it's not the big deal it used to be, but it still contains no data, so returning objects is superior because it can provide error context and the error information can be chained together as flows up the stack. I was horrified at go's error handling policy until I learned multiple values could be returned from functions, that gets rid of the main problem with using error returns in C.


As much as I like Go, I don't think we need to have an article about it every day, especially one that is several months old and contains wildly out of date statements like "panics are always fatal".

http://golang.org/ref/spec#Handling_panics


Thanks for pointing out that this differs from the specification.

I found this quite thought-provoking about different error handling techniques, so thought it worth submitting.


I fail to see how this is any better or worse than Java checked exceptions. You've got an error, you return an alternate value, which is handled by a different part of the code. Here is a different syntax to semantically write the exact same code, it's obviously superior.


The difference is that the Go style can provide a sane fallback, which the client code can IGNORE.

For instance, the built in map type returns a pair of values, the first is the actual matched value, the 2nd a boolean. If a key is not found the value is set to the type's zero-value, and the boolean as false. So for a map of string->integer, instead of throwing a NoSuchKey exception or something, just returns 0,false. So there's no mandatory ceremony when you don't care.


Sure, and you've got to check the boolean if you care. Which is almost the same as the (gasp) Java code:

    // don't care
    value = map.get(key);
    do bleh

    // care
    if (map.containsKey(key)) {
      do blah
    } else {
      value = map.get(key);
      do bleh
    }
To be clear, there are two kinds of errors:

1. The code is screwed up. Java runtime exception, log and hope someone is paying attention to the logs.

2. The world is screwed up. Java checked exception, catch and return a nice message to the user.


It appears your code will find the value _twice_. That is less than ideal and not equivalent at all.


If an assumption can be made that you won't put null values into the map then it can be rewritten where it doesn't need to do the check described above:

  value = map.get(key);
  if (null == value) {
    // Handle no value in the map
  }
Any class correctly implementing Map in Java won't throw an exception if there is no value for an input key (http://docs.oracle.com/javase/6/docs/api/java/util/Map.html#...) and some don't allow you to put null values in to begin with (http://docs.oracle.com/javase/6/docs/api/java/util/Hashtable...).


I think one of the points of the OP's article is that in Go you don't have to code like you've done here, where a single return value means one of two things (in your example: value in the map, or special reserved value that indicates the key wasn't found in the map). Go is cleaner, even if it's just a small improvement.


The argument used to be how "Go got exceptions right".

Now, it appears that the argument is about how to save a trivial amount of work for the CPU, when it's unclear whether this is a bottleneck, or whether the JIT can optimize away the two calls, or, in the remaining 1% of the cases, using the null idiom makes this a non-issue.

All at the cost of spending 1 of my 7 brain registers to handle "mandatory ceremony" of adding a _ to every map.get call, regardless of whether I actually know that the given map access should never see an unmapped key, which is the vast majority of cases in my day-to-day coding experience.

At the very best, the argument is about "multiple return values are useful in some cases". But tread carefully, lest every caller has to worry about useless return values.


Actually I was struck the other way: this appears to be basically unchecked exceptions. Functions return errors, which you can check or not if you like. The only difference is that the "exception" doesn't unwind the stack any more than the function call.

So this guy goes on about how bad unchecked exceptions are, then waxes poetic about Go's, erm, unchecked exceptions.

To add to the weirdness, he seems to think that most people like checked exceptions, when in fact he appears to be very much in the minority.


No library in a language with exceptions as they're normally used would do that. Exceptions are for exceptional cases. You use the method that throws only if it is a programming error for it to throw; and you do not, as a general rule, catch exceptions. See eg Dictionary.TryGetValue in .net. Your argument is against a straw man, IMHO.


So are you saying python is not a sane language?

    >>> a = {}
    >>> a['foo']
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    KeyError: 'foo'


Python uses Exceptions for unexceptional cases, yes, like loop termination. It's one of the only languages to do it.


It is widely agreed that this behavior is a hack in Python. It is not a role model.


Am I the only one who is saying "What the heck is Go and where did it come from?!" All of a sudden, in the past month or two I am seeing a lot of talk about Go, out of thin air. What's the rage about? Is it really going to take over the world?!


Resistance is futile! It is a slick language. It hit version 1.0 in March, which is probably why you're hearing so much about it now. Its concurrency model lets you keep all the machine's processor cores busy, without you going insane; that's worth the price of admission for me.


Do you see any use for Go in Web Development?


I'm coding a web app in it right now. Go includes a package for its own web server. That's a huge benefit, because it means the app can be state-ful instead of the usual (for Python, PHP etc.) stateless. The former means you can have all the data you want ready to go, in the most efficient memory structure, when the user arrives. Which translates to great performance. For example check out http://langalot.com and somewhat slowly type in "bark".


Do you see it as a competitor technology to things like Node.js / Meteor?


Yes. Node.js supports only a single processor core. I don't know much about Meteor but it doesn't look like it offers concurrency features.

The concurrency in Go (and to a lesser extent in Node.js--probably) is amazingly performant. For example, when I poll & parse 100 RSS feeds and the slowest one takes 6 seconds to return from the remote site, the whole routine returns in < 7 seconds. A synchronous single-core app would take up to 6 seconds * 100 = 10 minutes for the same process. In Go, when I call out to the remote sites, each call returns immediately, so that all 100 remote calls finish immediately. Go puts my routine into a dormant state while waiting for the remote sites to return data. So during that 7 seconds my app may have ~5 seconds of dormancy to do other processing (in a completely independent code path), again using all the machine's cores.


FastCGI gives you this feature in any compatible language.


That's good to know, thanks. I may want to use Nginx with Go someday, in a stateful way. The built-in web server may not be ready for prime time, I've read.


The big complication in Go that makes exceptions tricky is, "Who is my real caller?"

If you're in a goroutine, is your caller the place your goroutine was launched from, or whoever is hooked up at the other end of the channel that is expecting you to respond? In some sense it is both. Where would you propagate an exception? How do you choose?

Returning exception values provides an answer - it is up to you.


See links and monitors in Erlang as another way to do it, tried and tested for decades in production code.


Erlang's error handling is very well thought out. In fact, error handling is the main reason Erlang is shaped the way it is.

It is a combination of Go's technique but with panics for exceptional errors.

Like Go, functions should return error values for known errors. For instance, `file:open/2` returns `{error, enoent | eacces | eisdir | enotdir | enospc}` for known error conditions that can be handled. Unknown, exceptional conditions, like a NAS going down, can't be compensated for because the set of unknown errors is unbounded. There could literally be an infinite number of things that can go wrong that you don't know how to handle. For try/catch style error handling, the only safe way to handle the unknown errors is to do a Pokemon catch clause globally or around every single potentially error causing function call.

Because of the fact that you can't stop errors from happening is why Erlang has processes. When an unknown error occurs, the individual process dies and anyone monitoring that process is notified. OTP has supervisors whose sole job is to watch over processes and restart them if they crash.

Any state that needs to be preserved between restarts is stored somewhere safe (Erlangers calls this the "error kernel") so that when the process comes back it'll resume where it left off.

Once you structure your code to "Let it Crash", your code gets much simpler and safer. #1, the error return values forces you to compensate for known errors that can happen with a function, #2 you have to think about data durability in order to "Let it Crash". #3 Supervisors take care of the rest.

Data durability is made much easier with the fact that individual processes are single threaded and data is immutable, this makes them transactional by nature.


I think the language that gets Exceptions right is Common Lisp because of the restarts. With other languages handling exceptions is complicated. You either need to know internals of the code you are using or you simply bail out and print and error. With restarts you can simply offer ways to fix it and keep the logic where it belongs, close to the "throw".


the timeline C -> C++ -> Java -> Go is ridiculous at best, and this ignores work on exceptions or error handling in countless other languages that could have made it better.


Wrong, the timeline C -> C++ -> Java -> Go is more or less what the industry went through (well, assuming it's getting to Go now and it'll catch on).

It correctly ignores the work on "countless other languages", because they didn't have any if at all traction to the trade, and don't matter much as far as 90% of programmers are concerned, and the few languages outside those that do (Perl, PHP, Ruby, C#, JS, Python, VB) are quite similar to the above.


The primary question to answer when choosing between using error codes or exceptions for signalling errors is this: do you want the error propagation code to be written by one or more humans, or do you want the compiler to generate invisible but consistent error propagation code?


The obvious problem with the multiple return paradigm is that Go returns:

result AND error

whereas it would make much more sense to return:

result OR error

After all, if the error is true the result is invalid. This is one thing exceptions basically gets right. Go, much like the errno paradigm, relies on the programmer's diligence to know when it is safe to use the result. A better designed language takes away the option of accidentally doing something wrong.

Besides exceptions, this better approach can be accomplished by returning a union type variable that knows its current type (languages outside the C lineage call this sum types or tagged unions).

  var foo = Bar();
  if( typeof(foo) == Error ) {
    // handle error
  }
  else {
    // result is safe to use
  }


>After all, if the error is true the result is invalid.

False. A returned error does not necessarily mean the result is invalid. It could also mean something went wrong and you only have a partial result (which can be useful). Or that whatever the function returns doesn't directly have anything to do with what the function does (mostly for functions which are called for their side effects).

An example I can spontaneously come up with is the very common Writer interface. Its Write() function returns an int and an error. The int denotes the amount of bytes written to whatever is implementing the interface. This value is useful regardless of whether there was an error or not. You don't want an OR here. You want an AND.

Besides, what you are proposing could easily be done in Go. After all, errors are only things implementing the Error interface. But that doesn't really add to code clarity. Also, your code snippet isn't that different from how Go error handling looks right now. I don't see the practical difference.


I disagree. Java tried to make you handle the error (checked exceptions) and all that happened was people were irritated by it and caught and ignored them. I think at some point you have to expect the programmer to take some initiative in dealing with errors.


Conceptually that makes sense, but having to explicitly check the type of the return value every time is just hideous.


Uhm, then don't code in Go?


You can ignore errors in Go by assigning them to _. You can't if your return value is sometimes an Error.


You could implement something like lvalue pattern matching, if you wanted to.

Assuming you construct tagged unions like so:

    Valid 123
    Error "Error message"
You could pattern match on the left side of an assignment like so:

    Valid foo = dosomething()
    use(foo)
Which would extract the wrapped value into 'foo' if the union's tag was 'Valid', or aborted/panicked/did something else somewhat sane if the value didn't match.


If my return value is an Exception, I handle it in a catch block.


I find it interesting that they express result and error as a product rather than a sum.


There is a mailing list thread somewhere that explains that the Go designers did not understand the difference between sum types and untagged unions, and hence inappropriately rejected sum types.


That explains it. Interesting.


Go's error handling reminds me of the Icon programming language's elegant "goal-directed execution". Icon's control structures use implicit "success/failure" instead of true/false boolean logic. For example:

  if a := read() then write(a)
read() could return a false or zero value and still "succeed", calling write(a). If read() returns "failure", then write(a) will not be called. (I don't recall what the value of `a` will be in that case.)

https://en.wikipedia.org/wiki/Icon_programming_language#Goal...


That is like return codes in shell.


This approach made me think of MFC calls. Instead of having multiple return values you'd have a single return value and multiple out parameters.

It's been years since I've done any MFC work, but calls like:

HWND myWin = GetTopWindowTitle(region, out title, out size, out whateverElse);

Then you'd have to either assume that myWin was not an error code and just use the out parameters, or you'd explicitly check the myWin value each time.


This is a lot like how node.js handles Errors and exceptions only its "panic" call is throwing an Error either intentionally or via an uncaught exception, which will crash the process and is difficult to catch due to node.js' asynchronous nature. Apart from that, every function call returns both an error and a value, and the value is irrelevant in the presence of an Error.


I'm not familiar with Go.

If my code performs several operations in a row, any of which could return an error, do I have to check the returned error value after each one?

For example if performing file or network I/O?


Perhaps depending on how important it is for that piece of code to be flawless, but generally yes.

The upside of all that is that you can craft a piece of code to be practically flawless, since you know how the libraries you are using are supposed to break if they break.


I would love to see some code to illustrate this article, as someone who hasn't written anything in Go I still don't know what these mechanisms look like in practice.





Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: