9 ms·
Here's why Go's errors are better than exceptions: Multithreaded environments make exception handling next to impossible to do correctly. The stack trace only
by NateDad 12y ago
Here's why Go's errors are better than exceptions:
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.
- dllthomas 12y agoYour exception code ignores RAII, which is about the best argument for exceptions. The file should be closed as soon as the function returns for any reason, which (with a sensible design and properly implemented destructors) will be as soon as the function returns for any reason, meaning the outside try-finally is entirely unnecessary and the file is guaranteed freed even if you have multiple returns.
- vardump 12y agoGolang has defer. http://golang.org/doc/effective_go.html#defer http://golang.org/doc/effective_go.html#defer 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...
- dllthomas 12y agoI wasn't saying "Therefore, C++ is better than go", just that the code above was unnecessarily ugly. I actually think that exceptional conditions are usually better handled with return values, though ideally in a way that allows some constrained bubbling without too much boiler plate plumbing. Of languages I'm deeply familiar with, I think Haskell has the best facilities for this (mark it in the type system, hide it behind composition, require it's been handled in controlled places) but has facilities for crazy stuff too (some of which winds up unavoidable).
- vardump 12y agoHaskell's type system does sound interesting. Been on my list to check out for a long time, but so little time. 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.
- dllthomas 12y agoHappy to hear about your good experience :)
- dllthomas 12y agoThe downside to defer over RAII, of course, is that there's no static guarantee that you 1) remembered to include the defer, or 2) deferred the right thing. I could see that getting painful in some refactorings. Which isn't to say there aren't upsides.
- vardump 12y agoWhenever you do forget, it's immediately obvious by looking at the function in question. Or by grepping things that "open" something to see if there's corresponding defer nearby. 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.
- NateDad 12y agoRAII can get rid of that one try/finally, that's true. I was writing standard Java/C#/Python-eque code, however, which requires all those try/catches. 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.
- dllthomas 12y agoSure, and of course RAII can be of benefit either way. I'm not a pro-exceptions partisan, just don't like extraneously ugly code in examples. :-P
- NateDad 12y agoHaha, fair enough. But I was trying to write standard code that you'd see in many popular languages, whose proponents often complain about Go's error handling.
- dllthomas 12y agoFor sure. What I don't like about go's error handling is that it's product, not sum. That's a small objection, though.
- dragonwriter 12y ago> I was writing standard Java/C#/Python-eque code, however, which requires all those try/catches. 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) }
- WorldWideWayne 12y agoAnd here's what it might have looked like if they had taken notes from a language with a good exception handling style: 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.
- amatheus 12y agoThe code will close the file in case of a panic?