9 ms·
To each their own. I'm not going to claim to be an expert, but as somebody who's been coding since the 80s it was a breath of fresh air to see Go do what I wan
by bmurphy1976 2y ago
To each their own. I'm not going to claim to be an expert, but as somebody who's been coding since the 80s it was a breath of fresh air to see Go do what I wanted languages to do all long instead of ramming exceptions down my throat. I have problems with Go (examples: slice behavior and nil type interfaces) but error handling is not one of them.
- CharlieDigital 2y agoWhat challenge did you run into with exception handling? I'm curious because I've never felt it being onerous nor felt like there was much friction. Perhaps because I've primarily built web applications and web APIs, it's very common to simply let the exception bubble up to global middleware and handle it at a single point (log, wrap/transform). Then most of the code doesn't really care about exceptions. The only case where I might add explicit exception handling probably falls into a handful of use cases when there is a desire to a) retry, b) log some local data at the site of failure, c) perform earlier transform before rethrowing up, d) some cleanup, e) discard/ignore it because the exception doesn't matter.
- ratorx 2y agoIn Go, b) is really common. Most of my code will annotate a lower error with the context of the operation that was happening. You’ll ideally see errors at the top level like: “failed to process item ‘foo’: unable to open user database at ‘/some/path’: file does not exist” as an example. Here, the lowest level IO error (which could be quite unhelpful, because at best it can tell you the name of the file, but not WHY it’s being opened) is wrapped with the exact type of the file being opened (a user database) and why the database is being opened (some part of processing ‘foo’, could even generate better error message here). Although this is a bit of work (but in the grand scheme of things, not that much), it generates much better debugging info than a stack trace in a lot of situations, especially for non-transient errors because you can annotate things with method arguments. I think the common complaint of ‘if err != nil { return err }’ is generally not the case because well-written Go will usually prepend context to why the operation was being performed.
- codr7 2y agoIt's perfectly possible, and a lot less work, to wrap exceptions on their way up the call stack. The difference is you have to remember it at EVERY SINGLE freaking call site in Go.
- CharlieDigital 2y agoI don't see how this is different from exceptions though? Exceptions just make it optional if you want to handle it at that level (and you can).
- throwaway2037 2y ago> Most of my code will annotate a lower error with the context of the operation that was happening. This is easy to solve with chained exceptions to add context. > it generates much better debugging info than a stack trace in a lot of situations, especially for non-transient errors because you can annotate things with method arguments. You cannot add method args to an exception message? I am confused.
- ratorx 2y agoIt is definitely possible with exceptions, but it is not the norm (you can do it yourself, but will a library also do it?) because the norm in Java is to silently pass up exceptions as that is the most ergonomic thing. And once you start doing it with exceptions, there’s not much difference in the code you end up writing between errors and exceptions. In practice, I’ve found that when I write Go, I end up annotating most error returns, so the benefit of exceptions for me would be minimal.
- Capricorn2481 2y ago> And once you start doing it with exceptions, there’s not much difference in the code you end up writing between errors and exceptions The difference is where you want to catch the error, and not doing a bunch of "plumbing code" for intermediate callers that don't need to know about that error. > because the norm in Java is to silently pass up exceptions as that is the most ergonomic thing Adding args to an exception is completely localized. Adding additional args to an error in Go could mean changing dozens of files. Not to mention I can actually make my own exceptions for the problem. They are like enums with data.
- khana 2y ago[dead]
- hedora 2y agoExceptions are fine if you never catch them. So is calling abort(). (Which is the Unix way to do what you described.) If you need to handle errors, you quickly get into extremely complicated control flow that you now have to test: // all functions can throw, return nil or not. // All require cleanup. try { a = f(); b = a.g(); } catch(e) { c = h(); } finally { if a cleanup_a() // can throw if b cleanup_b() // null check doesn’t suffice… if c cleanup_c() } Try mapping all the paths through that mess. It’s 6 lines of code. 4 paths can get into the catch block. 8 can get to finally. Finally multiplies that out by some annoying factor.
- CharlieDigital 2y agoCan you give the equivalent in Go?
- ted_dunning 2y ago[flagged]
- CharlieDigital 2y agoThis is satire, right?
- ted_dunning 2y agoNo. It is just an explicit rendition of a complex topic. Do you have a more economical example that handles all of the corner cases of cleanup routines throwing errors?
- coder543 2y agoI recommend ignoring the other reply you just got. They are clearly building a bad faith argument to try to make Go look terrible while claiming to sing its praises. That is not at all how that would look in Go. The point being made was that the exception-based code has lots of hidden gotchas, and being more explicit makes the control flow more obvious. Something like this: a, err := f() if err != nil { c, err := h() if err != nil { return fmt.Errorf("h failed: %w", err) } cleanupC(c) return fmt.Errorf("f failed: %w", err) } defer cleanupA(a) b, err := a.g() if err != nil { c, err := h() if err != nil { return fmt.Errorf("h failed: %w", err) } cleanupC(c) return fmt.Errorf("a.g failed: %w", err) } defer cleanupB(b) // the rest of the function continues after here It’s not crazy. With Java’s checked exceptions, you at least have the compiler helping you to know (most of) what needs to be handled, compared to languages that just expect you to find out what exceptions explode through guess and check… but some would argue that you should only handle the happy path and let the entire handler die when something goes wrong. I generally prefer the control flow that languages like Rust, Go, and Swift use. Errors are rarely exceptional. Why should we use exceptions to handle all errors? Most errors are extremely expected, the same as any other value. I’m somewhat sympathetic to the Erlang/Elixir philosophy of “let it crash”, where you have virtually no error handling in most of your code (from what I understand), but it is a very different set of trade offs.
- ted_dunning 2y agoWell, my favorite nightmare with exceptions was code that was littered with throw NoopException() You tell me what that did.
- kgeist 2y agoHaving exceptions means that every line in your function is a potential exit point, often without you being aware of it. This can lead to bugs when a non-atomic operation is abruptly terminated, and you might not realize it just by glancing at the code. When we were rewriting some code from PHP to Go, I remember that simply thinking about "what to do with err" led me to realize we had a ticking time bomb - one that could explode in production. We had never realized that a certain line of PHP code could potentially throw an exception, and letting it bubble up the stack would have resulted in data corruption. With Go's explicit error handling, this issue became immediately obvious.
- gf000 2y agoGo can abort at any point as well. Also, ignoring an error condition (by either forgetting about it, or simply doing the nice and tidy if err dance with no real error handling in place, just a log or whatever) is much worse and can lead to silent data corruption.
- Mawr 2y agoYou can abort via panic(), but that is expected to crash the application, which is perfectly fine. It's the act of attempting to catch the abort that is fraught with problems. While in Go that is rare, in languages with exceptions it's normal and expected. Ignoring an error condition is possible in Go, but so unlikely that it's not practical to worry about it. As an aside, not that it matters, but logging an error is one of the valid ways of handling it, depending on context.
- gf000 2y agoLogging can be a valid handling of error. I just don't believe that most errors can be handled locally so besides returning an error (bubbling up), not much can be done. Go makes this part of the happy path, so neither can be easily seen/reasoned about anymore. Exceptions do auto bubbling up, while languages with ADTs have more strictness than go, and often have some syntactic sugar/macro to help with the common case of returning the error (rust's ?).
- Mawr 2y agoHow can you "let the exception bubble up" when you don't know what "the exception" is nor where it's going to be thrown? The agency you imply here does not exist. All you can do is what you've described - catch all exceptions at the top level and log them. It's a valid strategy so long as you don't mind your service going down at 3am because someone didn't realize that the call to Foo() on line 5593 could in fact throw exception Bar. Explicit error handling would make it obvious that Foo() can error, allowing the programmer to do whatever's appropriate, say implement a retry loop. Said programmer would then be sound asleep at 3am instead of playing whack-the-exception.
- CharlieDigital 2y agoNothing in languages that use exceptions prevent you for catching the generic exception type and handling it if you can recover/clean up. As I stated, my own rule of thumb (point (d) above) with respect to handling exceptions is "when I can/need to do something". If I can't do anything, then just bubble it up and log. Case in point is accessing a remote REST endpoint where I might be throttled or the service might be down. I can do something (retry) so I'll write code that probably looks similar to Go with Err: var retries = 0; do { try { result = await makeServiceCall(); } catch { if (retries == 3) throw; retries++; await Task.Delay(retries * 1000); } } while (retries < 3) The exception bubbling mechanism just makes it optional so you determine when you want to stop the bubbling.
- blindriver 2y agoNo, I disagree with you. If you are using Go, you have no idea what the error you're going to receive is, because the code you call could be calling other code that you have no idea about. You might use existing code that gets modified to call into another module and then you're going to get a whole set of errors that you aren't expecting and won't be able to react to. What this means is that you have NO way to handle errors except to just error out and bubble up. Because all you can do is look at the error, the best you can do is throw your hands up and say "okay just returning this error." How is this any better than an exception?