9 ms·
Errors vs. exceptions in Go and C++ in 2020
- mseepgood 6y agoAre they going to do this again after Go has switched to a register-based calling convention? https://go.googlesource.com/proposal/+/refs/changes/78/248178/1/design/40724-register-calling.md https://go.googlesource.com/proposal/+/refs/changes/78/24817...
- knz42 6y agoAbsolutely yes.
- marcus_holmes 6y agoI think they missed the point of Go's convention. It's designed to force people to handle the damn error as near to the call as possible. I've seen way too many programs with a single exception handler right at the base of the program, that just goes "whoops, something bad happened, bye!". I've even seen this anti-pattern used with Go's panic-recover mechanism. It's an interesting find though, that the actual performance cost for checking the error return is random, variable, and small. Good to know :)
- jcelerier 6y ago> It's designed to force people to handle the damn error as near to the call as possible. which is sometimes impossible to do in any meaningful way which just leads people to put panic in there making the end-user experience much worse than having an exception handler at the base of the program / event loop
- dgellow 6y agoIn production code people put panics around? I’ve never seen a situation like this. The convention to not use panic is quite strong
- knz42 6y agoAnd yet it certainly is there.
- andreygrehov 6y agoPanics are rare events that very few functions should ever need to think about. If the library truly cannot set itself up, it might be reasonable to panic (which is why panics are usually in the `main` package only). Once all the invariants have been checked, there is no reason to panic anytime after.
- akvadrako 6y agoPanic works fine; they are basically just exceptions you can catch at a higher level. I almost exclusively use panic for my exceptions in go.
- marcus_holmes 6y agoI stopped, because it conflicts so badly with the Go conventions, and because I found myself handling more errors when I didn't use panic. I think this is one of those things that new Gophers find hard to adjust to, and older ones realise the wisdom of (there's a few of these in the Go learning journey!). I'm not impugning your expertise or implying that you're inexperienced. It's just something I've noticed.
- akvadrako 6y agoFor me it was the other direction. I started using err returns because I knew it was idiomatic, but after doing that for months and seeing no benefit I decided it was just idiotic to continue. When I want to handle an error case locally I still use them, but that's extremely rare.
- knz42 6y ago> that the actual performance cost for checking the error return is random, variable, and small. Good to know :) That is certainly not the article's conclusion. The cost is deterministic, constant and non-negligible.
- Bootvis 6y agoAre you the author? I ask because the domain is similar to your username.
- Rochus 6y ago> and non-negligible This is probably a matter of discretion. Considering the overall performance of Go applications compared to other languages, 4 to 10% is quite low. The measurement error might also be a few percent.
- marcus_holmes 6y ago> Previously, in Go 1.10, this fixed cost was non-negligible, climbing upwards of dozens of nanoseconds. Thanks to recent improvements in the Go compiler however, as well as general improvements in CPU micro-architectures, this cost has been greatly reduced in 2020. I read that as "used to be non-negligable, is now negligable" 4%-10% depending on compiler and architecture is pretty variable, to my way of thinking. YMMV. also kinda random, in that there's nothing I can do in the code to determine how much overhead it costs, or change that (apart from ignoring Go's convention on error handling completely, which I'm not going to do because it wasn't a convention for performance reasons in the first place).
- knz42 6y agoYou're mis-reading the text. The reduction in cost pertains to the try/catch (defer/recover) mechanism, not error returns. The cost of error returns has not reduced since Go 1.10.
- marcus_holmes 6y agoAh ok, thanks for correcting my mis-apprehension :)
- jasode 6y ago>It's designed to force people to handle the damn error as near to the call as possible. But that is sometimes the wrong design. If you have functions A() --> call B() --> call C() ... and C() has an error because of a memory allocation failure or a network connection being down, sometimes the best context to handle that error is the outermost function A() and not C(). That's why some programmers don't like copypasting a bunch of "if err != nil {return err}" boilerplate across layers when the intentional semantic design is to deliberately autopropagate errors up the stack. E.g. function A() might have more knowledge of the state of the world via code logic to decide whether to retry a broken network connection or simply log the error and exit. Sometimes handling the error is orthogonal to how a nested call tree is structured. It depends.
- grey-area 6y agoSure but the advantage of explicit returns is that you can easily see where the error is returned and also add context to it. The disadvantage is a little more repeated code, which isn’t IMO a huge burden. With exceptions it is harder to know where or if it might be handled.
- kitkat_new 6y agoHow is it easier to know where or if it might be handled? If you return a error, you still don't know nothing, but that a caller in the chain to the bottom might handle it. There is no difference compared to exceptions, except you know that every caller will have to deal with boiler plate no matter if he is interested.
- grey-area 6y agoErrors must be passed manually up a call stack if they are not handled, so in practice this encourages handling them as soon as possible. That makes it easier to find where the error is handled. Exceptions are automatically passed up, so in practice are often caught by one catcher at the top level which is not very useful and has no idea what to do with the error. It's a very different mechanism. There is certainly more boilerplate with the Go approach.
- frou_dh 6y agoGo's thing is more "encourage" to handle errors than "force", given that the compiler has nothing to say about unhandled errors in the presence of certain variable reuse patterns, or completely unassigned returns. https://play.golang.org/p/mu5fbUrV322 https://play.golang.org/p/mu5fbUrV322
- dgellow 6y agoThat’s the correct way to present it IMHO. With Go you’re encouraged to deal with the error directly (two choices: return it, or do something about it), so that when reading you can follow what is happening at any time. When reading a Go function you can always say for sure if an error occurs with a given call and how it is handled. If for some reasons the project consider that checking errors should be enforced, that’s simple to do by using go-lint or other linters.
- marcus_holmes 6y agotrue, good point. And having the power to ignore the convention is good, too.
- masklinn 6y ago> And having the power to ignore the convention is good, too. Mistakenly not handling errors is not "the power to ignore the convention", it's "the language is half assed". "The power to ignore conventions" is being allowed but having to explicitly ignore the error, aka that the second and third cases trigger errors, and that you'd have to write: y, _ := fmt.Println("Bar") println(y) _, _ = fmt.Println("Qux") (also note how you can not use `:=` in the second case, because that requires that there be at least one new variable on the LHS)
- marcus_holmes 6y agoThe language allows you to ignore the convention. There is a plethora of tools available to allow you to detect if you did that when you didn't mean to. It's just that the compiler doesn't enforce it.
- coldtea 6y ago>It's designed to force people to handle the damn error as near to the call as possible. If they wanted to "force people" they could use Optionals and really force them. This no more forcing than mandating checked exceptions -- the user can just return the err immediately, like in Java they can just add a throws and propagate for others to handle, or an empty try/catch and ignore it...
- dgellow 6y agoYou have go-lint or other linters to enforce it. It’s a per-project choice.
- coldtea 6y agoIt's either a "per-project/leave it to the linter" choice, or a "they did this in the language to force people to handle errors close to the source". Can't have it both ways!
- dgellow 6y agoThe “force” is the part that is incorrect, or at least misleading. The go approach is to encourage people to handle it at the call site, and you can use extra tooling to enforce it. Once you get that point, there is no contradiction.
- Someone 6y agoIMO, Optionals or Either are superior to returning a pair (value, error) because they cannot return both a value and an error, thus removing one possible cause of bugs (likely a fairly small one! As it doesn’t prevent a function from constructing both a result and an error and only returning the error), but I don’t see how optionals force handling errors more than returning a pair (value, error). Surely, you can just check whether the optional has a value, use it when it is available, and ignore the other case.
- grandinj 6y ago> too many programs with a single exception handler right For a number of useful applications, this is exactly the right, correct, and most useful approach. I currently maintain several successful (within our commercial niche) 100kLOC+ programs that largely use such an architecture. It puts the error-handling code in one place, and enables common logging, recovery, filtering and display. It means that the vast majority of the code can happily just assume that the world is full of unicorns and light. And given that it is written in Java, the program just largely keeps on running, even in the presence of bugs and weird edge cases, and suchlike, a feature our users really like. Human are pretty good at going "OK, so that part of the program is having a bad day, I'll report the bug and keep on using the rest of the program".
- gpderetta 6y agoactually the best way is not to catch them. Let the application abort and leave a core file you can inspect with full stack trace from the throw point [1] and context. [1] I routinely remove "catch and rethrow" from our code base exactly for this reason. There are ways to log and add metadata to in flight exceptions that don't require rethrowing.
- otabdeveloper4 6y ago> It's designed to force people to handle the damn error as near to the call as possible. This is always the wrong way to handle errors. If a function returns an 'error' that needs be handled at the call site, then it isn't an error, it's a variant return type. Errors are things that can't be recovered from but must be handled to release resources. You want this to happen in some central place, not scattered ad-hoc in every place where you use resources; releasing them by hand is worse than manual memory management.
- dgellow 6y agoI think there is a misunderstanding. There isn’t just a single type of errors. Every time you get an error object in Go you ask yourself “should I do something about it or not”. If no then you add some context and return it to the parent, that’s a perfectly valid way to handle it. Otherwise you do your specific piece of logic to recreate your ressources or whatever is needed. Not all errors require the same treatment and there isn’t a single strategy to manage them.
- gumby 6y ago> I've seen way too many programs with a single exception handler right at the base of the program, that just goes "whoops, something bad happened, bye!". Regardless of one's view of execution handling, why would anyone even bother to do this? If you don't catch it and exit the program will exit anyway.
- dgellow 6y agoTo have cleaner logs maybe? Stack trace are often a mess to parse. Or at least correctly close resources such as DB connections before exiting?
- masklinn 6y ago> It's designed to force people to handle the damn error as near to the call as possible. Except for not even remotely doing that: 1. if a call can fail but returns no useful value (or the caller cares little about it, and thus ignores everything it returns), Go will not complain that you're ignoring the return value entirely 2. if you have several calls which can fail, nothing forces you to actually handle all the errors, because Go doesn't check for that, it relies on the compiler error that a variable must be used: v1, err := Foo(false) if err != nil { fmt.Println("error") return } fmt.Println("first", v1) v2, err := Foo(true) fmt.Println("second", v2) will not trigger any error, because the second calls simply reassigns to the existing `err`, which has already been used once, and thus is fine by the compiler.
- marcus_holmes 6y agoYeah it's a convention. It's not enforced by the compiler. It is caught by several of the static code checking tools (and some linters I believe). You can ignore the convention if you want (you probably shouldn't, but you can). You could make the case that this is a footgun, sure. I prefer to think of it as giving me the right tools to make the right choice in my specific circumstances.
- asdfasgasdgasdg 6y agoThe point of Go's convention isn't really relevant to the question of its relative cost compared to exceptions, is it? I don't see that they so much missed it as didn't evaluate it.
- kcartlidge 6y agoHow dare you remain on topic.
- knz42 6y agoFYI these results were presented at the Go Systems Conf SF last December: https://www.youtube.com/watch?v=inrqE0Grgk0&t=15126s https://www.youtube.com/watch?v=inrqE0Grgk0&t=15126s
- deleted 6y ago[deleted]
- tankenmate 6y agoThe one item I'd really contend is where it says it "makes it easier to ... maintain over time". That might be true for smaller code bases (tracking down exceptions generated from libraries called from libraries, fun!), or code bases where you don't use closed external libraries (that can generate unknowable exceptions), or you use only synchronous code (because asynchronous exceptions wind up jumping to fishkill, welcome to distributed systems (logically, physically or chronologically distributed)). [EDIT] fixed thinko
- DoctorNick 6y agothank you for emulating the experience of debugging distributed systems
- knz42 6y agoThis was clarified in the conf talk [1] [2]: error returns should be used at API boundaries, and panic-driven handling only "within" a component. [1] https://www.youtube.com/watch?v=inrqE0Grgk0&t=15126s https://www.youtube.com/watch?v=inrqE0Grgk0&t=15126s [2] https://docs.google.com/presentation/d/1WVu4O-ax7punUC2V_XgTA0VqZReToo1sNXA5IaHFqdE/edit https://docs.google.com/presentation/d/1WVu4O-ax7punUC2V_XgT...
- tankenmate 6y agoFor simplicities sake I just use error returns everywhere and then you have a consistent error handling abstraction (and one that scales to boot).
- trinovantes 6y agoThis is one of the reasons why I really like monorepos. Tracking down an opaque error from 2 network hops away is a nightmare compared to reading an exception in a call stack
- enriquto 6y agoAs a famous software philosopher said (I think it was Uriel): errors are wrong. Or, to put it more clearly: there are no errors, only conditions that you dislike. It's better to not burden your programming with your emotional shortcomings, and treat all conditions that you may encounter on an equal footing. You try to open a file; the file may or may not exist, and both cases are equally likely and you get to decide what your program does in each case. No need to attach an emotionally charged label like "error" in one of the two cases of the conditional. Or worse, as some emotional fanatics do, to bend an otherwise clean programming language by adding features (e.g., exceptions) that help support your sentimental disposition.
- asdfasgasdgasdg 6y ago> both cases are equally likely Both cases are not equally likely, though. Also, this article is not about the philosophical approach to naming errors versus exceptions. It's about the performance of two technical approaches to handling exceptional/unlikely circumstances.
- enriquto 6y ago> Both cases are not equally likely, though. Of course, if you call fopen with uniformly distributed random filenames then it is extremely unlikely than such files will exist. Thus it will fail with probability essentially 1. Yet, I don't want my programming language to force me to make an asymmetric distinction between the two cases. By "equally likely" I don't mean "having equal probability to occur". This is very difficult to model, and it will depend mostly on the usage patterns of the users of the program. I mean that both cases are worth of the same attention and merit an equivalently serious treatment. No need to disparage one of the two cases as an "error" or an "exception" and require a special language construct.
- asdfasgasdgasdg 6y agoI assure you, the file-does-not-exist case cares nothing for whether you "disparage" it by calling it an error. As for having a special language construct, the performance benefits discussed in this article are one example of why you might want to have one. If you already have it, using it to represent file not found excursions seems reasonable.
- peterohler 6y agoGreat article! As a performance dweeb, any information on how best to squeeze out a bit more performance is welcome. I might have to play with using panics in OjG (https://github.com/ohler55/ojg https://github.com/ohler55/ojg) and see if it gives a boost.