10 ms·
Discussion: Reduce error handling boilerplate in Golang using '?'
- cratermoon 2y agoThis is from Ian Lance Taylor, a major figure in the development of Go. Taylor was instrumental in bringing generics to the language, this proposal is worth taking seriously.
- 9rx 2y ago> Taylor was instrumental in bringing generics to the language, this proposal is worth taking seriously. He submitted, what, 8 failed generics proposals before Phil Wadler came in to figure out what he was missing? I don't mean to diminish what he has done. He is clearly an important contributor and even those failed proposals were important steps along the way. What I do mean is that judging a proposal based on who it is written by is silly. Imagine if one of those early generics proposals were taken seriously just because of who he is. I expect even he would be unhappy about that outcome in hindsight. The author is irrelevant. If it is a good proposal, it can stand on its own merits.
- jpalawaga 2y agoIt's worth noting that 'taking a proposal seriously' doesn't equate to accepting it.
- cratermoon 2y agoThanks for your insights, they are both penetrating and diffuse.
- RedNifre 2y ago(I'm not a Go programmer) I find this a bit odd. Isn't the idea of the primitive error handling that it is obvious and easy, as in "functions can return multiple results, a popular pattern is to return the good result and the error as two separate nullable values of which exactly one will be not null, so you can check if err == nil."? If you go with fancy error handling anyway, how is this '?' better than returning a Result and do something like foo().getOr { return fmt.Errorf("Tja: %v", err) }
- pornel 2y agoThe ? syntax agrees that errors should just be regular values returned from functions, and handling of errors should be locally explicit. It's not a different approach from `if err != nil return err`, it merely codifies the existing practice, and makes expressing the most common cases more convenient and clearer. It's clearer because when you see ? you know it's returning the error in the standard way, and it can't be some subtly different variation (like checking err, but returning err2 or a non-nil ok value). The code around it also becomes clearer, because you can see the happy path that isn't chopped up by error branches, so you get high signal to noise ratio, fewer variables in the scope, without losing the error handling.
- RedNifre 2y agoI think what I meant is that out of these three ways to do it, I don't like the second one: 1. The language has the feature of returning multiple values, which is then used in an error handling pattern. 2. The proposal is about special syntax for that pattern 3. Languages that have generics (like Go) can instead implement a Result type, turning this pattern into more readable code without the extra syntax. I feel like a Result type would have more advantages and be less disruptive than a syntax macro for a pattern, but I'm not sure.
- teeray 2y agoI feel like error handling in Go is divided between people who have been using the language for a long time, and those who are new to it. If you're used to exceptions, and languages with some kind of '?' operator, typing `if err != nil` all the time is probably excruciating. They seem to be the most vocal in the survey about wanting beloved error handling features from their favorite languages. Once you've been using the language for awhile, you begin to dislike the elaborate system of rugs other languages have to sweep errors under. Errors in Go are right there, in your face, and undeniable that the operation you are doing can be faulty somehow. With good error wrapping, you can trace down exactly which of these `if err != nil` blocks generated the error without a stack trace. If it bothers you that much, you can always make a snippet / macro for it in your editor.
- memset 2y agoSuper interested in your approach to error wrapping! It’s a feature I haven’t used much. I tend to use logs with line numbers to point to where errors occur (but that only gets me so far if I’m returning the error from a child function in the call stack.)
- teeray 2y agoSimply wrap with what you were trying to do when the error occurred (and only that, no speculating what the error could be or indicate). If you do this down the call stack, you end up with a progressive chain of detail with strings you can grep for. For example, something like "processing users index: listing users: consulting redis cache: no route to host" is great. Just use `fmt.Errorf("some wrapping: %w", err)` the whole way up. It has all the detail you want with none of the detail you don't need.
- toxik 2y agoSo hand rolled call stack traces? I just don’t understand why this is better than exceptions.
- Swizec 2y agoWhere I've found `?` super helpful in JS/TS and now miss it the most in Python is dealing with nested data structures. ``` if (foo.bar?.baz?.[5]?.bazinga?.value) ``` Is so much nicer than ``` if foo.bar and foo.bar.baz and foo.bar.baz[5] and foo.bar.baz[5].bazinga and foo.bar.baz[5].bazinga.value ``` I honestly don't care which one of those is falsy, my logic is the same either way. This enables you to ergonomically pass around meaningful domain-oriented objects, which is nice. Edit: looks like optional chaining is a separate proposal – https://github.com/golang/go/issues/42847 https://github.com/golang/go/issues/42847
- theamk 2y agoIt saddens me tbat the default error handler is "return err", as opposed to something that appends context/stack trace. We've converted a few scripts and webapps from Python to Go, and if one does default handling ("return err") the error logs became significantly less useful, compared to exception backtraces. Yes, there are ways around it, but most tutorials don't show them.
- Ferret7446 2y agoStack traces are expensive. You need to make a conscious decision whether you want a stack trace attached to a particular error (and possibly where it gets attached, if you want it higher in the call chain), which aligns with Go's design philosophy for error handling.
- theamk 2y agoC++-style stack traces, with stack walking and ELF section analysis, are expensive. Python's stack traces, which also involve reading source files from disk, are expensive as well. But go's do not have to be. A compiler can expand "foo()?" to something like: err := foo(); if err != nil { return err.WithStringContext("foo() in MyFile.go:25"); } The only complexity there is appending a constant pointer to "err", and this only happens in error case that uses "?". Depending on implementation it could be a single word write, if compiler can prove there are no other users of "err". (And if your code is carefully written to be allocation-free and appending a pointer kills that? In this case, you don't have to use "?", put "return err" directly.)
- pphysch 2y ago5 years ago I would be more sympathetic to this proposal. But now we have LLM copilots, so writing boilerplate in any language is dramatically more optional. And I don't see this proposal significantly improving readability of the code.
- xigoi 2y agoThe boilerplate does not go away just because an LLM can write it. It still makes the code harder to read.
- jchw 2y agoMy knee jerk reaction is that introducing even more ways to write the same thing is going to slowly bloat the language, but Go does it infrequently enough that it doesn't seem like it's going to become a huge problem. I think I could get used to this syntax. > Disadvantage 4: No other block in Go is optional. The semicolon insertion rule, and the fact that a block is permitted where a statement is permitted, means that inserting or removing a newline can convert one valid Go program into another. As far as I know, that is not true today. Yeah, this seems like a big problem to me, personally. Go has a fair number of lingering foot guns but this is one too far IMO. I think the no-block case should require something else to follow it, perhaps the return keyword. That'd also help prevent it from being as easily missed...
- vrnvu 2y agoUnfortunately, every software project will eventually reach a point of maturity where more and more features are added simply for the sake of adding them. "The goal of this proposal is to introduce a new syntax that reduces the amount of code required to check errors in the normal case, without obscuring flow of control." The key is "check errors in the normal case". When the core principles of Go have always been simplicity, flexibility, and having one way of doing things, this feels completely like a step in the opposite direction. We will have syntax sugar for "normal cases" while still relying on the `if err != nil` block for everything else. It’s similar to how we now have both `iterators` and `for loops` as constructions for loops.
- pjmlp 2y agoWhat is happening is that Go designers are discovering that the "academic" features of the language that predated it for decades have gotten them for a reason.
- TriangleEdge 2y agoSeems like the author assumes that only one error is returned. What if I want to return []error? What happens if my return objects are out of the normal order? Like F() (int, error, int) {...}. The proposal is nice, but a bit shallow.
- thangngoc89 2y ago> What happens if my return objects are out of the normal order? Like F() (int, error, int) {...} The convention is that error should be the last return value. If the error is not nil, then discard other returned values.
- qaq 2y agohonestly with copilot and friends this is not really an issue
- rigelbm 2y agoError handling is really not an issue that needs fixing in Golang. That being said, I wish Golang had an assert key word as a shortcut to "if cond { panic }". A lot of those "if err != nil" in the wild should really just be assertions.
- ikiris 2y agoThere are 0 times I’d want to panic in go code in production services.
- floating-io 2y agoThe biggest issue I have with this proposal is that reading the code in naïve fashion comes up with the wrong answer for me; YMMV. The proposed form-- foo ? { bar } Reads naturally to me as "If foo then bar", when it's actually "If foo's error return exists then bar". I would suggest a different operator character, because this one reads wrongly IMO. Maybe it's just because I originally come from C, where `foo ? bar : baz` meant "if foo then bar else baz", but the fact remains...
- chairhairair 2y agoIt could be: foo ?! { bar } But, now we’re potentially confusing the “negation” and the “exclamation” meanings the bang symbol usually tries to communicate.
- nonethewiser 2y agoI tend to agree that ? looks like "if then" when what we really want is some sort of coalescing, or "if not then". foo ?? { bar } foo ?/ { bar } foo ?: { bar } foo ?> { bar } foo ||> { bar } Im not sure I like the idea at all though. It seems like a hack around a pretty explicit design choice. Although I do tend to agree the error handling boilerplate is kind of annoying.
- jayd16 2y agoHow about this? :) { foo } catch { bar }
- nonethewiser 2y agoThis is probably the one that would anger Golangers the most lol
- jayd16 2y agoThey could even have typed error checking for different errors and an automatic return error syntax. { Foo } catch (err) {} catch (err2){} throw
- jasonthorsness 2y agoThe proposal confuses shadowing of err, which is mostly irrelevant anyway but at least you can see it today. It also makes breakpoints or inserting log statements etc. more difficult without reformatting code. And lastly, it teases the ternary conditional operator that Go lacks, constantly reminding me of this. So IMO I would use a different language rather than adopt this.
- high_na_euv 2y agoIt is crazy that error handling is one of the most important things, yet even modern languages suck at it. In my opinion everything should return type like MayFail<T>, Result<T>
- sigmonsays 2y agoI vote no on this proposal. Go error handling should remain simple, like the language. These are all tools, just pick the one you like and stop trying to make them like others.
- echelon 2y ago> These are all tools, just pick the one you like and stop trying to make them like others. I'm not so sure I agree with that. I'm glad Rust continues to evolve at a healthy pace and pick up new syntactic features. Boilerplate is a sin against the brain. As long as you don't pay it down with increased cognitive complexity, it should be eliminated at all costs. Error handling is such an essential feature of problem solving that this should be one of the highest priorities for a language to get right. It should be so simple and elegant that you can bash it out in a few tokens, and almost impossible to get wrong. I think Go would be a much better language if it took this to task. I reach for Rust for out of domain problems because it's so expressive and intentional and safe and concise.
- eweise 2y agoBy that logic, Go shouldn't have generics but they've added a lot of value to our codebase.
- nonethewiser 2y agoGo is an incomplete language masquerading as a simple one.
- pjmlp 2y agoC should not care for safety, there are tools, pick the one you like....
- reactordev 2y agoThis breaks go's readability and explicit nature. No thanks. The author doesn't understand the implications of the proposal. What if the args are in a different order returned? "foo, error" or "error, *foo" ? - I've seen many permutations of this. Being explicit about error handling is actually a good thing.
- 827a 2y agoFor clarity: The author is Ian Lance Taylor. He's a principal engineer at Google, on the Golang team, and the 4th largest contributor to the language. The personal attack on him not understanding the implications of the proposal is a bit cringe.
- reactordev 2y agoI know who he is, I stand by my statement that the proposal is ill-advised.
- zimbatm 2y agoThis change would mark a turn in the language's evolution, as it would be the first implicit variable to be added to the language. I'm not going to invoke the slippery slope argument, but what distinguishes Go from the pack is how explicit it is. It can make it more tedious to write, but also much easier to follow as a reader.
- zemo 2y agothere's some precedent in the direction of adding predeclared identifiers for error handling: the identifier `error` was originally not predeclared, you had to import it from `io` (or maybe `os`?) and refer to it as `io.Error` everywhere.
- pjmlp 2y agoIt is not more explicit than C, Pascal, JOVIAL and many other predating it for decades.
- evanmoran 2y agoIf I may make a suggestion to @ianlancetaylor, I think using the ? for error checking is a fantastic idea, but I think a couple small changes would make this absolutely a game changer and even more Go-like: To demonstrate my tweak to your idea, imagine this example code: r, err := SomeFunction() if err != nil { return fmt.Errorf("something 1 failed: %v", err) } r2, err := SomeFunction2() if err != nil { return fmt.Errorf("something 2 failed: %v", err) } r3, err := SomeFunction3() if err != nil { return fmt.Errorf("something 3 failed: %v", err) } In the current proposal it would turn into this: r := SomeFunction() ? { return fmt.Errorf("something 1 failed: %v", err) } r2 := SomeFunction2() ? { return fmt.Errorf("something 2 failed: %v", err) } r3 := SomeFunction3() ? { return fmt.Errorf("something 3 failed: %v", err) } My first suggestion is to keep `err` variables visible. It ends up being not much longer, but it is much more readable and Go-like: r, err := SomeFunction() ? { return fmt.Errorf("something 1 failed: %v", err) } r2, err := SomeFunction2() ? { return fmt.Errorf("something 2 failed: %v", err) } r3, err := SomeFunction3() ? { return fmt.Errorf("something 3 failed: %v", err) } My second suggestion is to require ? to always have a block, and also allow them to "chain" so only the last statement needs a block: r, err := SomeFunction() ? r2, err := SomeFunction2() ? r3, err := SomeFunction3() ? { return fmt.Errorf("something 1, 2 or 3 failed: %v", err) } As you can see this is much shorter! Having the block is always required at the end of the "chain" of question mark statements is more consistent with how `if` statements require a block currently. It also makes the `return err` flow also always visible (no return magic). It also also has a huge advantage of it being much harder to miss a question mark syntactically. as a question mark without a block would be a syntax error. For example, this is an error: r, err := SomeFunction() ? // <-- compile error: missing block after ? And also this is an error: r, err := SomeFunction() ? r2, err := SomeFunction2() // <-- compile error: missing block after ? r3, err := SomeFunction3() ? { return fmt.Errorf("something 1, 2 or 3 failed: %v", err) } Thanks for listening! Curious what folks think.
- 827a 2y agoI overall like it and would prefer a world where Go had this spec implemented versus did not. Criticism: > Within the block a new variable err is implicitly declared, possibly shadowing other variables named err Shadowing here is strange, and I would prefer a design where it did not shadow other variables named err, but rather threw a compiler error concerning the re-declaration of a variable. That would effectively mean that you can't mix-and-match this syntax with old error-handling inside one function, because code like this would fail to compile: func Test() { user, err := GetUser("12345") if err != nil { panic(err) } EmailUser(user) ? { panic(err) } } I'm fearful the shadowing will be confusing, because one might try to reference that shadowed error within the block in (rare) situations where you need to return the synthesis of two error values, and you'll need to know the trivia of: `err` is a special name, I shouldn't name that shadowed variable `err`, let me name it `err2`. Granted: throwing a compiler error would also disallow this and force you to name the first variable `err2`; but at least the compiler is telling you the problem, rather than relying on your knowledge of new trivia.
- bpt3 2y agoI don't care for this spec and probably wouldn't use it if it were implemented, but I do like your suggestion of how to handle err shadowing.
- thiht 2y agoI really hate that a bare ? makes us lose some info on code coverage. If you only test the happy path, the line is counted as covered. Making the return explicit at least makes it obvious when a line is uncovered. But my biggest beef is the implicit variable declaration, I can’t stand it. That’s just lazy, bad design. That’s not a great proposal overall, and I suspect if the same proposal had been made by someone else outside of the Go core team, we would have not heard of it. I hope it gets rejected.