27 ms·
I'm sure I'm in a very tiny minority, but I kinda like having errors as values - I prefer having errors be things that are part of the normal control flow of my
by liampulles 3y ago
I'm sure I'm in a very tiny minority, but I kinda like having errors as values - I prefer having errors be things that are part of the normal control flow of my code, and that can be handled explicitly within an if block (which may start simple but can and does evolve to wrap details, log and skip, fallback to an alternative, etc.)
Yes it means errors shows up everywhere in your code - the same types of errors are something all languages have to deal with, just accept our faulty reality for what it is and have the discipline to account for it.
- xyzzy_plugh 3y agoI'd be surprised if you're in a minority -- maybe here that's true but most Go developers I know and work with are pretty content with the state of affairs regarding error handling.
- morelisp 3y agoMost Java developers I know and work with are pretty content with the state of affairs regarding error handling. But it sucks too.
- threeseed 3y agoJava is significantly better than Go. You have the option to explicitly handle every error, not handle errors for certain methods, or bubble up errors to a single error handler or any combination thereof.
- bb88 3y agoWell RuntimeErrors (NullPointerExceptions's and the like) weren't never explicitly required to be on the method signature. I was trying to remember who it was, but one author I thought had recommended subclassing every exception as a Runtime exception for this reason.
- threeseed 3y agoYes there are checked and unchecked exceptions. NullPointerException don't really make sense as checked because almost every method will have some exposure to null values. But the idea is that encapsulating methods can check for those and translate them into other exception types or just let it be handled by a global exception handler. Java is not the best for error handling by any stretch. But it's easily better than Go.
- free652 3y ago>Java is not the best for error handling by any stretch. But it's easily better than Go. Nah. If you ever worked in a big enough company you would have: try & catch Throwable at the main entry. Go forces to think about every error. C# doesn't even have checked errors, after using Java for 10 years - checked exceptions are dumb. Go doesn't have a stack trace in the error and thats dumb.
- majormajor 3y ago> Go forces to think about every error. Unless you just always return it up the chain without thinking about it like the bulk of the code I've encountered. No more thoughtful than "catch and reraise" or "just throw." And like in Java, an NPE ain't getting caught by the error return of a function in Go either.
- masklinn 3y agoEven if you ignore this, assume nobody ever created an editor macro to smash, go has several glaring holes which make it very possible (and reasonably easy) to miss errors entirely. And unlike Java, those errors are just gone, they won’t be breaking noisily eventually.
- threeseed 3y agoNot sure what you mean there. With checked exceptions you need to either handle them directly or yes you can wrap them in a RuntimeException and catch it in main. I've done a decade of old-school JVM development at enterprise companies. Never seen any codebase where no exceptions are locally handed.
- morelisp 3y agoQED.
- gaganyaan 3y agoThat's the blub paradox in action
- Smaug123 3y agoSame with the Go developers I know, which means it falls to me to raise the bugs against their stuff ;)
- starttoaster 3y agoGo developer here. I'm pretty content with the state of error handling.
- metaltyphoon 3y agoDo some Go on the side, I’m not content and Go yearly survey shows many are not content either as it’s the #1 complaint.
- bb88 3y agoI do too. Error handling is fine-ish mostly. The issue I have is there are three forms of error handling. Errors : Fine. Panics : Weird because golang already had errors. Segfaults: Gah! (though this is a cgo issue apparently) See https://news.ycombinator.com/item?id=37908655 https://news.ycombinator.com/item?id=37908655 and https://rachelbythebay.com/w/2023/10/16/env/ https://rachelbythebay.com/w/2023/10/16/env/
- randomdata 3y ago> Panics : Weird because golang already had errors. It is true that panic allows any value to propagate, so technically you can use it to carry errors, along with anything else you can imagine (names, email addresses, audio, whatever). But the intent is for it to be used for communicating exceptions. You will notice panic's behaviour mirrors exception handling systems found in some other popular languages. But errors, along with names and email addresses for that matter, are decidedly not exceptional.
- Cthulhu_ 3y agoPanics are discouraged to use though, or, only when you intend to have the whole app crash immediately, which is rarely the case. It's there, but you shouldn't use them in practice.
- metaltyphoon 3y agoPanics recovery should almost always be used on web services. You do want them to crash a web server.
- jjnoakes 3y agoI wear two hats when I develop in go. One is for robust long-lived systems and for those I don't mind verbose error handling. The other is small quick scripts and for those I really wish I had a non-verbose option.
- Cthulhu_ 3y agoA few years ago there was an effort to come up with improving error handling, but after several months of back and forth, various concepts, etc etc, the consensus was that the existing error handling wasn't too bad, and/or the proposed alternatives weren't significantly better.
- masklinn 3y agoProviding richer tools to manipulate results and errors doesn’t make them not-values.
- liampulles 3y agoYou are right. I think what I don't like is that using this operator stops you being able to use the error as a value in the given context. I would rather always have the if block there, screaming it me, ready to be addressed. But certainly YMMV.
- deagle50 3y agoI think there's an easy solution to this, would love to know if I'm off track: postfix `catch` optionally captures the last value in the return tuple. Instead of value, err := foo(); if err != nil { // handle err }; you get value := foo() catch err { // handle err }
- otteromkram 3y agoWe have: if value, err := foo(); err { // do something with err. } else { // do something with value. }
- deagle50 3y ago"else" for the happy path is part of the problem.
- neilwilson 3y agoGenerally in Go the error path is part of the if, which usually returns from the function. Then the 'happy path' is just the rest of the function. If you end up with complexity at this point then it is time to refactor into several functions so the 'happy path' follows the standard idiom. Consider it a Go code smell.
- packetlost 3y agoI also like errors as values, I just wish there was a syntactically simpler way to express "let the caller of this function deal with the error".
- lionkor 3y agolike Zigs `try xyz();` which does what you say
- packetlost 3y agoOr Rust's `?`. The problem is Go returns errors by conventional multiple-returns/tuples, not by language construct.
- lionkor 3y agoit could do something sane but smart, like returning the error for whatever argument is an error type, and nil the rest.
- maccard 3y agoBoxed error typed like Result in rust, or optional in c++ are what go needs.
- tialaramex 3y agoC++ std::optional is not an error type. It's a (slower, bigger, less capable) Option. The C++ type intended to be similar to Rust's Result is std::expected
- maccard 3y agoThanks, sorry. std expected is exactly what I should have said. I didn't see your reply until after the edit window though.
- dragonwriter 3y agoMost Go criticism I've seen on error handling has been “if you are going to use errors-as-values, sum types and pattern matching were the well established approach to that when Go was created and multivalued returns when only one value is usually going to be meaningful at a time is both logically and ergonomically worse” not “don't use errors as values”.
- teaearlgraycold 3y agoBasically - use Rust instead.
- danesparza 3y agoWhy? This doesn't look particularly more or less friendly in Rust than in any other programming language: https://doc.rust-lang.org/book/ch09-00-error-handling.html https://doc.rust-lang.org/book/ch09-00-error-handling.html
- LelouBil 3y agoA lot of common error handling patterns are expressed more effecively in Rust. For bubbling up errors you have the ? operator, then on results you have map, map_err, and, ok_and...
- ColonelPhantom 3y agoAside from the other reply which points out that common idioms have useful shorthands like ?, Rust's type system is also error-aware. Rust errors are actual sum types (either Ok(T) or Err(E)), while Go just uses multiple return/tuples to return (value, nil) or (nil, error). This means that in Rust, you are forced to handle errors (well, unless you don't care about the return value, but then the compiler still gives a warning). This means it is harder to make a mistake with error checking.
- monocasa 3y agoIt also enforces the lack of weird anti patterns like some functions in go that will return a significant value in the first return value while also returning non nil in the error, with a snarky comment in the docs about why this actually makes sense in this case.
- jrockway 3y agoBeing able to wrap the error with context is the key. If done right, you can identify the exactly what caused the error without any additional infrastructure. (For people that say stack traces do this, they don't. They don't tell you which iteration of the for loop you're on. They don't tell you what the code on the other side of the channel you read the error from was doing.) My rule for errors is to add only information that the caller isn't aware of. So don't do: func GetFooByID(ctx context.Context, id int) (Foo, error) { row, err := LookupRow(ctx, "foo", id) if err != nil { return nil, errors.Wrapf(err, "GetFooByID id=%v", id) } return row, nil } The caller already knows the id it's looking up, and that the name of the function is GetFooByID. It might include those in its wrapping of that error, but GetFooByID shouldn't. Meanwhile, this is good: func UpgradeFoos(ctx context.Context, tx *Tx) error { foos, err := GetAll(ctx, tx, "foos") if err != nil { return errors.Wrap(err, "GetAll") } for _, foo := range foos { if err := UpgradeFoo(ctx, tx, foo.ID); err != nil { return errors.Wrapf(err, "UpgradeFoo(%v)", foo.ID) } } return nil } Now your logs look something like "server failed to startup error=apply migration: upgrade foos: UpgradeFoo(42): i/o timeout" instead of "server failed to startup error=i/o timeout". This saves you from "hmm, maybe it's too slow to get all the foos in a batch like that, we should change that". But nope, it's actually UpgradeFoo(42) that's broken. Wrapping applied well is my favorite feature of Go. Most people don't do it. The standard library doesn't follow my rules. But if you do it this way, every error you see is so easy to debug and resolve. Less downtime, more reliability. (As an aside, that loop where you accumulate errors can easily accumulate into a multi-error as of recent versions of Go. I always prefer to try everything possible and return all the errors, then you can fix multiple problems with your input on one go. It's also good for cases where you are doing a main operation and an ancillary operation, like closing something. People often ignore errors on "Close" and "Sync", but by joining those into a multierror, then you no longer have questions like "there were no errors, but this file isn't on disk". https://pkg.go.dev/go.uber.org/multierr#hdr-Deferred_Functions https://pkg.go.dev/go.uber.org/multierr#hdr-Deferred_Functio... is a really nice approach for the common case of "defer fh.Close()". I have an `errors.Close` wrapper I use: https://github.com/pachyderm/pachyderm/blob/master/src/internal/errors/multi.go#L26 https://github.com/pachyderm/pachyderm/blob/master/src/inter.... Can't live without it!)
- deleted 3y ago[deleted]
- deagle50 3y agofoo().unwrap_or_else(|err| { // handle err }); or foo() catch |err| { // handle err }
- tail_exchange 3y agoI like the second example, but without the |, so no new syntax is necessary. I don't like the first first one. It's too verbose, and it looks like it's trying to introduce a shorthand for lambdas.
- deagle50 3y agoWas just showing how Rust and Zig do it. Go capture could omit the ||.
- tail_exchange 3y agoI've seen proposals that were very similar. I think the problem with this approach is that it doesn't really make error handling that much easier. Typing "catch err" is not that different from typing "if err != nil".
- Cthulhu_ 3y agoIn Go, if a function returns only an error, you can do if err := foo(); err != nil { // handle error } However, this only works in that particular case, if foo() also returns a value you can no longer use this because the value (and err) are scoped to the if block.
- akira2501 3y agoDue to the nature of my work I'm often in a position to write, deploy and then maintain systems that are running my code and generally have to play the part of top tier support for the entire stack. The more that this has become my reality, the more I care about actually producing worthwhile errors. I'm not at all bothered that 70% of my code is error case management with some liberal sprinklings of context into those values. At this point, I see straight through them in my code, to the point where I'm actually going to be confronted with an error, bug or emergent system case and I'm going to be exceptionally pleased that I put so much effort into actually managing these errors correctly and not just punting them up through a common and often cryptic common error handler case with most of the useful context about the detailed error environment now missing.
- beautron 3y agoThis is exactly how I feel about Go error handling (which I prefer to other languages). Error checks become a syntactic formalism that are easy and quick to type, easy and quick for the eye to scan, but have just enough presence to make sure you think through your error situation whenever and wherever you need to. Go gives me a confidence in my code's error handling that is harder to get in other languages I've used, where the error situation is typically muddier.
- thayne 3y agoI like having errors as values, as is the case in rust, and haskell, and idiomatic scala. What i don't like is where approximately 3/4 of the significant lines of code are endless repetitions of: if err { return nil, err } Rust has ?, haskell has do notation, scala has for. All of them have higher order functions for operating on the result. But go is not only more verbose, it is more error prone, since it is easy to forget to check an error, or use the other returned value before checking the error.
- bb88 3y agoOr worse. Some library developer decided that "panic()" was the way to go rather than: return nil, err I'm fine with error values or Pythonesque exceptions, just not both combined. So much of my code is typically: result, err := someFunc() if err != nil { return nil, err } Most error conditions are basically unrecoverable anyway.
- randomdata 3y ago> approximately 3/4 of the significant lines of code are endless repetitions of: if err { return nil, err } 3/4? How are you managing to have so many cases of blindly passing an error up the stack without introducing problematic coupling? I would suggest that if you are able to realistically do this more than a couple of times total in an application, you have introduced way too much pointless indirection and should take a closer look at your overall design. Something is amiss. And that goes for any language – not something exclusive to Go. Blindly propagating an error up the stack using exception handlers, for example, is prone to the same problematic coupling and indicates the same design problems if seen in more than rare situations.
- dd82 3y agoits of course hyperbolic but frequent enough in any substantial codebase to be a wart on the language.
- randomdata 3y agoMost codebases would not justify the use of 'return nil, err' at all. The language certainly isn't going to stop you, but you are going to make life miserable for future developers if you do so. There are rare circumstances where it is the right thing, but if you are seeing more than one or two instances in a substantial codebase, something isn't right. If it is a common occurrence, and you are not purposefully trying to demonstrate your hatred of future developers, you've no doubt introduced way too much unnecessary complexity – which too is going to make life miserable for future developers.
- mlinhares 3y agoI hated it but I’ve grown into it, specially with the latest improvements and the https://pkg.go.dev/github.com/pkg/errors https://pkg.go.dev/github.com/pkg/errors package. It’s still not as good as having Either/Try monads available to avoid the mess of “if err != nil” madness but having gone back to Java lately and the issues we have with exceptions there, having errors as values instead of a magical control flow feature makes the code easier to understand. Now that I can make sure I include slack traces on them so it’s easy to find where they originated I have the best of exceptions in place as well.
- bananapub 3y ago> but I kinda like having errors as values this is the sort of silliness that is common in the Go world. as noted elsewhere, errors are values in lots of languages - Rust, C, Scala, Haskell, etc etc, but Go explicitly has no way to handle them nicely, no specific syntax and no fancy type system stuff like sum types. it is my very strong belief that this will eventually be fixed in Go and when it does, almost all the people currently saying "I like errors being values [and it's fine that Go makes it very annoying]" will quickly prefer having some actual language help for these values.
- tail_exchange 3y agoGo actually has something akin to Sum types for errors, called wrapping. For example: var innerErr = errors.New("inner error") func innerFunc() error { return innerErr } func outerFunc() error { err := innerFunc() return fmt.Errorf("outer err: %w", err) } func main() { err := outerFunc() fmt.Println(errors.Is(err, innerErr)) // true } I don't think the Go community disagrees that it would be nice to make error handling a bit easier, because that was the top issue raised in the recent developer survey. The problem is that every proposal made so far either did not actually make error handling that much better, by looking at the feedback they received.