go: proposal: Go 2: simplify error handling with || err suffix
There have been many proposals for how to simplify error handling in Go, all based on the general complaint that too much Go code contains the lines
if err != nil {
return err
}
I’m not sure that there is a problem here to be solved, but since it keeps coming up, I’m going to put out this idea.
One of the core problems with most suggestions for simplifying error handling is that they only simplify two ways of handling errors, but there are actually three:
- ignore the error
- return the error unmodified
- return the error with additional contextual information
It is already easy (perhaps too easy) to ignore the error (see #20803). Many existing proposals for error handling make it easier to return the error unmodified (e.g., #16225, #18721, #21146, #21155). Few make it easier to return the error with additional information.
This proposal is loosely based on the Perl and Bourne shell languages, fertile sources of language ideas. We introduce a new kind of statement, similar to an expression statement: a call expression followed by ||. The grammar is:
PrimaryExpr Arguments "||" Expression
Similarly we introduce a new kind of assignment statement:
ExpressionList assign_op PrimaryExpr Arguments "||" Expression
Although the grammar accepts any type after the || in the non-assignment case, the only permitted type is the predeclared type error. The expression following || must have a type assignable to error. It may not be a boolean type, not even a named boolean type assignable to error. (This latter restriction is required to make this proposal backward compatible with the existing language.)
These new kinds of statement is only permitted in the body of a function that has at least one result parameter, and the type of the last result parameter must be the predeclared type error. The function being called must similarly have at least one result parameter, and the type of the last result parameter must be the predeclared type error.
When executing these statements, the call expression is evaluated as usual. If it is an assignment statement, the call results are assigned to the left-hand side operands as usual. Then the last call result, which as described above must be of type error, is compared to nil. If the last call result is not nil, a return statement is implicitly executed. If the calling function has multiple results, the zero value is returned for all result but the last one. The expression following the || is returned as the last result. As described above, the last result of the calling function must have type error, and the expression must be assignable to type error.
In the non-assignment case, the expression is evaluated in a scope in which a new variable err is introduced and set to the value of the last result of the function call. This permits the expression to easily refer to the error returned by the call. In the assignment case, the expression is evaluated in the scope of the results of the call, and thus can refer to the error directly.
That is the complete proposal.
For example, the os.Chdir function is currently
func Chdir(dir string) error {
if e := syscall.Chdir(dir); e != nil {
return &PathError{"chdir", dir, e}
}
return nil
}
Under this proposal, it could be written as
func Chdir(dir string) error {
syscall.Chdir(dir) || &PathError{"chdir", dir, err}
return nil
}
I’m writing this proposal mainly to encourage people who want to simplify Go error handling to think about ways to make it easy to wrap context around errors, not just to return the error unmodified.
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Reactions: 303
- Comments: 519 (191 by maintainers)
A plain idea, with support for error decoration, but requiring a more drastic language change (obviously not for go1.10) is the introduction of a new
checkkeyword.It would have two forms:
check Aandcheck A, B.Both
AandBneed to beerror. The second form would only be used when error-decorating; people that do not need or wish to decorate their errors will use the simpler form.1st form (check A)
check AevaluatesA. Ifnil, it does nothing. If notnil,checkacts like areturn {<zero>}*, A.Examples
check, sobecomes
becomes
2nd form (check A, B)
check A, BevaluatesA. Ifnil, it does nothing. If notnil,checkacts like areturn {<zero>}*, B.This is for error-decorating needs. We still check on
A, but isBthat is used in the implicitreturn.Example
becomes
Notes
It’s a compilation error to
checkstatement on things that do not evaluate toerrorcheckin a function with return values not in the form{ type }*, errorThe two-expr form
check A, Bis short-circuited.Bis not evaluated ifAisnil.Notes on practicality
There’s support for decorating errors, but you pay for the clunkier
check A, Bsyntax only when you actually need to decorate errors.For the
if err != nil { return nil, nil, err }boilerplate (which is very common)check erris as brief as it could be without sacrificing clarity (see note on the syntax below).Notes on syntax
I’d argue that this kind of syntax (
check .., at the beginning of the line, similar to areturn) is a good way to eliminate error checking boilerplate without hiding the control flow disruption that implicit returns introduce.A downside of ideas like the
<do-stuff> || <handle-err>and<do-stuff> catch <handle-err>above, or thea, b = foo()?proposed in another thread, is that they hide the control flow modification in a way that makes the flow harder to follow; the former with|| <handle-err>machinery appended at the end of an otherwise plain-looking line, the latter with a little symbol that can appear everywhere, including in the middle and at the end of a plain-looking line of code, possibly multiple times.A
checkstatement will always be top-level in the current block, having the same prominence of other statements that modify the control flow (for example, an earlyreturn).Coincidentally, I just gave a lightning talk at GopherCon where I used (but didn’t seriously propose) a bit of syntax to aid in visualizing error-handling code. The idea is to put that code to the side to get it out of the way of the main flow, without resorting to any magic tricks to shorten the code. The result looks like
where
=:is the new bit of syntax, a mirror of:=that assigns in the other direction. Obviously we’d also need something for=as well, which is admittedly problematic. But the general idea is to make it easier for the reader to understand the happy path, without losing information.I really like the the syntax proposed by @billyh here
or more complex example using https://github.com/pkg/errors
While I’m not sure if I agree with the idea or the syntax, I have to give you credit for giving attention to adding context to errors before returning them.
This might be of interest to @davecheney, who wrote https://github.com/pkg/errors.
less is more…
Am I the only one that thinks all these proposed changes would be more complicated than the current form.
I think simplicity and brevity are not equal or interchangeable. Yes all these changes would be one or more lines shorter but would introduce operators or keywords which a user of the language would have to learn.
I’d like to suggest that any reasonable shortcut syntax for error-handling have three properties:
return err) easier than another. Sometimes an entirely different action might be preferable (like callingt.Fatal). We also don’t want to discourage people from adding additional context.Given those constraints, it seems that one nearly minimal syntax would be something like
For example,
which is equivalent to
Even though it’s not much shorter, the new syntax moves the error path out of the way. Part of the change would be to modify
gofmtso it doesn’t line-break one-line error-handling blocks, and it indents multi-line error-handling blocks past the opening}.We could make it a bit shorter by declaring the error variable in place with a special marker, like
How about expanding the use of Context to handle error? For instance, given the following definition:
Now in the error-prone function …
In the intermediate function …
And in the upper level function
There are several benefits using this approach. First, it doesn’t distract the reader from the main execution path. There is minimal “if” statements to indicate deviation from the main execution path.
Second, it doesn’t hide error. It is clear from the method signature that if it accepts ErrorContext, then the function may have errors. Inside the function, it uses the normal branching statements (eg. “if”) which shows how error is handled using normal Go code.
Third, error is automatically bubbled up to the interested party, which in this case is the context owner. Should there be an additional error processing, it will be clearly shown. For instance, let’s make some changes to the intermediate function to wrap any existing error:
Basically, you just write the error-handling code as needed. You don’t need to manually bubble them up.
Lastly, you as the function writer have a say whether the error should be handled. Using the current Go approach, it is easy to do this …
With the ErrorContext, you as the function owner can make the error checking optional with this:
Or make it compulsory with this:
If you make error handling compulsory and yet the user insists on ignoring error, they can still do that. However, they have to be very explicit about it (to prevent accidental ignore). For instance:
This approach changes nothing to the existing language.
I’m actually pretty excited that some people are focusing on making it easier to write correct code instead of merely shortening incorrect code.
When the return ExpressionList contains two ore more elements, how it works?
BTW, I want panicIf instead.
What about having real exception handling? I mean Try, catch, finally instead like many modern languages?
I don’t think I can write a comprehensive formal set of criteria. The best I can do is an incomplete list of things that matter that should only be disregarded if there is a significant benefit to doing so.
I don’t agree with the problem statement. I’d like to suggest an alternative:
Error handling does not exist from the language point of view. The only thing Go provides is a predeclared error type and even that is just for convenience because it does not enable anything really new. Errors are just values. Error handling is just normal user code. There is nothing special about it from the language POV and there should not be anything special about it. The only problem with error handling is that some people believe this valuable and beautiful simplicity must be eliminated at any cost.
The more I see these different proposals, the more I’m inclined to want a gofmt-only change. The language already has the power, let’s just make it more scannable. @billyh, not to pick on your suggestion in particular but
returnif(cond) ...is just a way of rewritingif cond { return ...}. Why can’t we just write the latter? We already know what it means.or even
or
No new magic keywords or syntax or operators.
(It might help if we also fix #377 to add some flexibility to the use of
:=.)Please bring back the Try Catch Finally and we don’t need anymore fights. It makes everyone happy. There is nothing wrong with borrowing features and syntaxes from other programming languages. Java did it and C# did that as well and they both are really successful programming languages. GO community (or authors) please be open to changes when it is needed.
Let’s not forget that errors are values in go and not some special type. There is nothing we can do to other structs that we can’t do to errors and the other way around. This means that if you understand structs in general, you understand errors and how they are handled (even If you think it is verbose)
This syntax would require new and old developers to learn a new bit of information before they can start to understand code that uses it.
That alone makes this proposal not worth it IMHO.
Personally I prefer this syntax
over
It is one line more but it separates the intended action from the error handling. This form is the most readable for me.
I can’t remember who originally proposed it, but here’s another syntax idea (everybody’s favorite bikeshed 😃. I’m not saying it’s a good one, but if we’re throwing ideas into the pot…
would be equivalent to:
and
would be equivalent to:
The
tryform has certainly been proposed before, a number of times, modulo superficial syntax differences. Thetry ... catchform is less often proposed, but it is clearly similar to @ALTree’scheck A, Bconstruct and @tandr’s follow-up suggestion. One difference is that this is an expression, not a statement, so that you can say:You could have multiple try/catches in a single statement:
although I don’t think we want to encourage that. You’d also need to be careful here about order of evaluation. For example, whether
h()is evaluated for side-effects ife()returns a non-nil error.Obviously, new keywords like
tryandcatchwould break Go 1.x compatibility.I think this mischaracterizes the attitude underlying what you are experiencing. Yes, the community produces a lot of push back when someone proposes try/catch or ?:. But the reason is not that we’re resistant to new ideas. We almost all have experience using languages with these features. We are quite familiar with them, and someone of us have used them on a daily basis for years. Our resistance is based on the fact that these are old ideas, not new ones. We already embraced a change: a change away from try/catch and a change away from using ?:. What we are resistant to is changing back to using these things we already used and didn’t like.
This issue started out as a specific proposal. We aren’t going to adopt that proposal. There has been a lot of great discussion on this issue, and I hope that people will pull the good ideas out into separate proposals and into discussion on the recent design draft. I’m going to close out this issue. Thanks for all the discussion.
It’s beautiful to add check, but you can do further before returning:
becomes
It also attempts simplifying the error handling (#26712):
It also attempts simplifying the (by some considered tedious) error handling (#21161). It would become:
Of course, you can use a
tryand other keywords instead of thecheck, if it’s clearer.Reference:
A plain idea, with support for error decoration, but requiring a more drastic language change (obviously not for go1.10) is the introduction of a new check keyword.
It would have two forms: check A and check A, B.
Both A and B need to be error. The second form would only be used when error-decorating; people that do not need or wish to decorate their errors will use the simpler form.
1st form (check A) check A evaluates A. If nil, it does nothing. If not nil, check acts like a return {<zero>}*, A.
Examples
If a function just returns an error, it can be used inline with check, so
becomes
For a function with multiple return values, you’ll need to assign, like we do now.
becomes
2nd form (check A, B) check A, B evaluates A. If nil, it does nothing. If not nil, check acts like a return {<zero>}*, B.
This is for error-decorating needs. We still check on A, but is B that is used in the implicit return.
Example
becomes
Notes It’s a compilation error to
use the check statement on things that do not evaluate to error use check in a function with return values not in the form { type }*, error The two-expr form check A, B is short-circuited. B is not evaluated if A is nil.
Notes on practicality There’s support for decorating errors, but you pay for the clunkier check A, B syntax only when you actually need to decorate errors.
For the
if err != nil { return nil, nil, err }boilerplate (which is very common) check err is as brief as it could be without sacrificing clarity (see note on the syntax below).Notes on syntax I’d argue that this kind of syntax (check …, at the beginning of the line, similar to a return) is a good way to eliminate error checking boilerplate without hiding the control flow disruption that implicit returns introduce.
A downside of ideas like the <do-stuff> || <handle-err> and <do-stuff> catch <handle-err> above, or the a, b = foo()? proposed in another thread, is that they hide the control flow modification in a way that makes the flow harder to follow; the former with || <handle-err> machinery appended at the end of an otherwise plain-looking line, the latter with a little symbol that can appear everywhere, including in the middle and at the end of a plain-looking line of code, possibly multiple times.
A check statement will always be top-level in the current block, having the same prominence of other statements that modify the control flow (for example, an early return).
On the other hand, the current way of error handling does have some merits in that it serves as a glaring reminder that you may be doing too many things in a single function, and some refactoring may be overdue.
@KamyarM You are confusing our requests that you remain polite with our disagreement with your arguments. When people disagree with you, you are responding in personal terms with comments like “Thank you so much for your thumbs down. That’s exactly the issue I am talking about. GO community is not open to changes and I feel it and I really don’t like that.”
Once again: please be polite. Stick to technical arguments. Avoid ad hominem arguments that attack people rather than ideas. If you genuine don’t understand what I mean, I am willing to discuss it offline; e-mail me. Thanks.
@lpar Yes, If Rust has a such a forum I would do that 😉 Seriously I would do that. Because I want my voice to be heard.
The justification for many of these proposals is better “ergonomics” but I don’t really see how any of these are better other than making it so there’s slightly less to type. How do these increase the maintainability of the code? The composability? The readability? The ease of understanding the control flow?
this particular bug can be fixed by introducing a double return facility to the language. in this case the function a() returns 123:
This facility can be used to simplify error handling as follows:
This allows people to write an error handler functions that can propagate the errors up the stack such error handling functions can be separate from main code
If I understand correctly, to build the syntax tree the parser would need to know the types of the variables to distinguish between
and
It doesn’t look good.
Since the bar for consideration is generally something that is hard to do in the language as is, I decided to see how hard this variant was to encode in the language. Not much harder than the others: https://play.golang.org/p/9B3Sr7kj39
I really dislike all of these proposals for making one type of value and one position in the return arguments special. This one is in some ways actually worse because it also makes
erra special name in this specific context.Though I certainly agree that people (including me!) should be wearier of returning errors without extra context.
When there are other return values, like
it can definitely get tiresome to read. I somewhat liked @nigeltao’s suggestion for
return ..., errin https://github.com/golang/go/issues/19642#issuecomment-288559297Here’s another thought.
Imagine an
againstatement which defines a macro with a label. The statement statement it labels can be expanded again by textual substitution later in the function (reminiscent of const/iota, with shades of goto :-] ).For example:
would be exactly equivalent to:
Note that the macro expansion has no arguments - this means that there should be less confusion about the fact that it is a macro, because the compiler doesn’t like symbols on their own.
Like the goto statement, the scope of the label is within the current function.
I like the explicitness of go error handling. The only trouble, imho, is how much space it takes. I would suggest 2 tweaks:
nilis equivalent tofalse, non-niltotrue&&and||So
can become
or even shorter
and, we can do
or, perhaps
If overloading
&&and||creates too much ambiguity, perhaps some other characters (no more than 2) can be selected, e.g.?and#or??and##, or??and!!, whatever. The point being to support a single-line conditional statement with minimal “noisy” characters (no needed parens, braces, etc.). The operators&&and||are good because this usage has precedent in other languages.This is not a proposal to support complex single-line conditional expressions, only single-line conditional statements.
Also, this is not a proposal to support a full range of “truthiness” ala some other languages. These conditionals would only support nil/non-nil, or booleans.
For these conditional operators, it may even be suitable to restrict to single variables and not support expressions. Anything more complex, or with an
elseclause would be handled with standardif ...constructs.@josharian, I’ll try to summarize the discussion briefly. It will be biased, since I have a proposal in the mix, and incomplete, since I’m not up to rereading the entire thread.
The problem we’re trying to address is the visual clutter caused by Go error handling. Here’s a good example (source):
Several commenters in this thread don’t think this needs fixing; they are happy that error-handling is intrusive, because dealing with errors is just as important as handling the non-error case. For them, none of the proposals here are worth it.
Proposals that do try to simplify code like this divide into a few groups.
Some propose a form of exception handling. Given that Go could have chosen exception handling at the beginning and chose not to, these seem unlikely to be accepted.
Many of the proposals here choose a default action, like returning from the function (the original proposal) or panicking, and suggest a bit of syntax that makes that action easy to express. In my opinion, all those proposals fail, because they privilege one action at the expense of others. I regularly use returns,
t.Fatalandlog.Fatalto handle errors, sometimes all in the same day.Other proposals provide no way to augment or wrap the original error, or make it significantly harder to wrap than not. These also are inadequate, since wrapping is the only way to add context to errors, and if we make it too easy to skip, it will be done even less frequently than it is now.
Most of the remaining proposals add some sugar and sometimes a bit of magic to simplify things without constraining the possible actions or the ability to wrap. My and @bcmills’s proposals add a minimal amount of sugar and zero magic to slightly increase readability, and also to prevent a nasty sort of bug.
A few other proposals add some sort of constrained non-local control flow, like an error-handling section at the beginning or end of a function.
Last but not least, @mpvl recognizes that error-handling can get very tricky in the presence of panics. He suggests a more radical change to Go error-handling to improve correctness as well as readability. He has a compelling argument, but in the end I think his cases don’t require drastic changes and can be handled with existing mechanisms.
Apologies to anyone whose ideas aren’t represented here.
@lpar So If I work for a company and for some unknown reason they picked GoLang as their programming language, I simply need to quit my job and apply for a Java one!? Come on man!
@bcmills You can count the code you suggested there. I think that’s 6 lines of code instead of one and probably you get couple of code cyclomatic complexity points for that (You guys use Linter. right?).
@carlmjohnson and @bcmills Any syntax that is old and mature doesn’t mean that is bad. Actually I think the if else syntax is way older than ternary operator syntax.
Good that you brought this GO idiom thing. I think that is just one of the issue of this language. Whenever there is request for a changes someone says oh no that’s against Go idiom. I see it as just an excuse to resist changes and block any new ideas.
To be honest, most of my professional engineering experience has been with PHP (I know) but the main attraction for Go was always the readability. While I do enjoy some aspects of PHP, the piece I despise the most is the “final” “abstract” “static” nonsense and applying overcomplicated concepts to a piece of code that does one thing.
Seeing this proposal gave me immediate flashback to the feeling of looking at a piece and having to do a double take and really “think” about what that piece of code is saying/doing. I do not think that this code is readable and does not really add to the language. My first instinct is to look on the left and I think this always returns
nil. However, with this change I would now have to look left, and right, to determine the behavior of the code, which means more time reading and more mental model.However, this does not mean there is no room for improvements to error handling in Go.
What happens in this code:
(My apologies if this isn’t even possible w/o the
|| &PathError{"chdir", dir, e}part; I’m trying to express that this feels like a confusing override of existing behavior, and implicit returns are… sneaky?)@cthackers I personally believe that it’s very nice for errors in Go to not have special treatment. They are simply values, and I think they should stay that way.
Also, try-catch (and similar constructs) is just all around a bad construct that encourages bad practices. Every error should be handled separately, not handled by some “catch all” error handler.
Can I add a suggestion also? What about something like this:
The error assignment can have any form, even be something stupid like
Or return a different set of values in case of error not just errors, And also a try catch block would be nice.
Why just don’t invent a wheel and use known
try..catchform as @mattn said before? )It seems like there is no reason to distinguish the catched error source, because if you really need this you could always use the old form of
if err != nilwithouttry..catch.Also, I do not really sure about that, but may be add the ability to “throw” an error if it does not handled?
Thank you so much for your thumbs down. That’s exactly the issue I am talking about. GO community is not open to changes and I feel it and I really don’t like that.
This is probably not related to this case but I was looking another day for Go equivalent of C++'s ternary operator and I came across to this alternative approach:
v := map[bool]int{true: first_expression, false: second_expression} [condition] instead of simply v= condition ? first_expression : second_expression;
Which one of the 2 forms you guys prefer? An unreadable code above (Go My Way) with probably lots of performance issues or the second simple syntax in C++ (Highway)? I prefer the highway guys. I don’t know about you.
So to summarize please bring new syntaxes , borrow them from other programming languages. There is nothing wrong with that.
Best Regards,
@KamyarM , I respectfully disagree; try/catch does not make everyone happy. Even if you wanted to implement that in your code, a thrown exception means that everyone who uses your code needs to handle exceptions. That is not a language change that can be localized to your code.
I suggest that we should squeeze the target of this proporsal. What issue will fixes by this proporsal? Reduce following three lines into two or one ? This might be change of language of return/if.
Or reduce the number of times to check err? It may be try/catch solution for this.
We already have less readable code because of the verbose error handling. Adding already mentioned Scanner API trick, that supposed to hide that verbosity, makes it even worse. Adding more complex syntax might help with readability, that what syntactic sugar is for in the end. Otherwise there’s no point in this discussion. The pattern of bubbling up an error and returning zero-value for everything else is common enough to warrant a language change, in my opinion.
How about a Swift like
guardstatement, except instead ofguard...elseit’sguard...return:If you’re willing to put some curly braces around that, we don’t need to change anything!
You can have all the custom handling you want (or none at all), you’ve saved two lines of code from the typical case, it’s 100% backwards compatible (because there’s no changes to the language), it’s more compact, and it’s easy. It hits many of Ian’s driving points. The only change might be the
gofmttooling?Slightly magic (feel free to -1) but supports mechanical translation
Go is open to criticism but we require that discussion in the Go forums be polite. Being frank is not the same as being impolite. See the “Gopher Values” section of https://golang.org/conduct. Thanks.
Error handling is very different from logging; one describes the program’s informative output, the other manages the program’s flow. If I set up the adaptor to do one thing in my package, that I need to properly manage the logical flow, and another package or program alters that, you are in a Bad Place.
I have to also suggest something like try/catch, where err is defined inside try{}, and if err is set to non-nil value - flow breaks from try{} to err handler blocks (if there are any).
Internally there are no exceptions, but the whole thing should be closer to syntax that do
if err != nil breakchecks after every line where err could be assigned. Eg:I know it looks like C++, but it is also well known and cleaner than manual
if err != nil {...}after every line.I think “must” functions are going proliferate out of desperation for more readable code.
sqlx
html template
I propose the panic operator (not sure if I should call it an ‘operator’)
same as, but maybe more immediate than
😱 is probably too difficult to type, we could use something like !, or !!, or <!>
Maybe !! panics and ! returns
Go is just a programming language. There are so many other languages. I found out that many in the Go Community look at it like their religion and are not open to change or admit the mistakes. This is wrong.
@rodcorsi Under this proposal your example would be accepted with no vet warning. It would be equivalent to
@billyh this is explained above, on the line that says:
checkwill return the zero value of any return value, except the error (in last position).Then you’ll use the
if err != nil {idiom.There are many cases where you’ll need a more sophisticate error-recovering procedure. For example you may need, after catching an error, to roll back something, or to write something on a log file. In all these cases, you’ll still have the usual
if erridiom in your toolbox, and you can use it to start a new block, where any kind of error-handling related operation, no matter how articulated, can be carried out.See my answer above. You’ll still have
ifand anything else the language gives you now.Maybe. But introducing opaque syntax, that requires syntax highlighting to be readable, is not ideal.
my idea:
|err|means check error : if err!=nil {}Here is a version closer to Rog’s proposal (I don’t like it as much):
Setting aside for a moment the idea that the proposal here has to be explicitly about error handling, what if Go introduced something like a
collectstatement?A
collectstatement would be of the formcollect [IDENT] [BLOCK STMT], where ident must me an in-scope variable of anil-able type. Within acollectstatement, a special variable_!is available as an alias for the variable being collected to._!cannot be used anywhere but as an assignment, same as_. Whenever_!is assigned to, an implicitnilcheck is performed, and if_!is not nil, the block ceases execution and continues with the rest of the code.Theoretically, this would look something like this:
which is equivalent to
Some nice other things a syntax feature like this would enable:
New syntax features required:
collect_!(I’ve played with this in the parser, it’s not hard to make this match as an ident without breaking anything else)The reason I’m suggesting something like this is because the “error handling is too repetitive” argument can be boiled down to “nil checks are too repetitive”. Go already has plenty of error handling features that work as-is. You can ignore an error with
_(or just not capture return values), you can return an error unmodified withif err != nil { return err }, or add context and return withif err != nil { return wrap(err) }. None of those methods, on their own, are too repetitive. The repetitiveness (obviously) comes from having to repeat these or similar syntax statements all over the code. I think introducing a way to execute statements until a non-nil value is encountered is a good way to keep error handling the same, but reduce the amount of boilerplate necessary to do so.check, since it stays the same (mostly)
check, as the error handling code can now go in one place if needed, while the meat of the function can happen in a linearly readable way
check, this is an addition and not an alteration
check, I think, since the mechanisms for error handling aren’t different - we’d just have a syntax for “collecting” the first non-nil value from a series of executions and assignments, which can be used to limit the number of places we have to write our error handling code in a function
I’m not sure this applies here, since the feature suggested applies to more than just error handling. I do think it can shorten and clarify code that can generate errors, without cluttering in nil checks and early returns
Agreed, and so it seems that a change whose scope extends beyond just error handling may be appropriate. I believe the underlying issue is that
nilchecks in go become repetitive and verbose, and it just so happenserroris anil-able type.@creker yes, I am totally agree with you. I was thinking about flow control in example above, but did not realize how to do this in a simple form.
Maybe something like:
Or other suggestions before like
try a := foo()…?Also, a general comment, unrelated to the recent discussion of try/catch.
There have been many proposals in this thread. Speaking for myself, I still do not feel that I have a strong grasp on the problem(s) to be solved. I would love to hear more about them.
I’d also be excited if someone wanted to take on the unenviable but important task of maintaining an organized and summarized list of problems that have been discussed.
The scanner types in the standard library store the error state inside of a structure whose methods can responsibly check for the existence of an error before proceeding.
By using types to store the error state, it’s possible to keep the code that uses such a type free of redundant error checks.
It also requires no creative and bizarre syntax changes or unexpected control transfers in the language.
People advocating for exceptions should read this article: https://ckwop.me.uk/Why-Exceptions-Suck.html
The reason why Java/C++ style exceptions are inherently bad has nothing to do with performance of particular implementations. Exceptions are bad because they are BASIC’s “on error goto”, with the gotos invisible in the context where they may take effect. Exceptions hide error handling away where you can easily forget it. Java’s checked exceptions were supposed to solve that problem, but in practice they didn’t because people just caught and ate the exceptions or dumped stack traces everywhere.
I write Java most weeks, and I emphatically do not want to see Java-style exceptions in Go, no matter how high performance they are.
IMO, the best proposal I have seen to achieve goals of “simplify error handling in Go” and “return the error with additional contextual information” is from @mrkaspa in #21732:
This doesn’t handle cases like bufio functions that return non-zero values as well as errors but I think it’s okay to do explicit error handling in cases where you care about the other return values. And of course the non-error return values would need to be the appropriate nil value for that type.
The ? modifier will reduce boilerplate error handing in functions and the ! modifier will do the same for places where assert would be used in other languages such as in some main functions.
This solution has the advantage of being very simple and not trying to do too much, but I think meets the requirements laid out in this proposal statement.
@romainmenke Nope. You’re not the only one. I fail to see the value of a one-liner error handling. You may save one line but add much more complexity. The problem is that in many of these proposals, the error handling part becomes hidden. The idea is not to make them less noticeable because error handling is important, but to make the code easier to read. Brevity doesn’t equal to easy readability. If you have to make changes to the existing error handling system, I find the conventional try-catch-finally is much more appealing than many ideas purposes here.
I think this discussion of adding a
trysugar to Rust would be illuminating to participants in this discussion.Just to make it clear that adding a new keyword to the language is a breaking change.
results in
prog.go:7:6: syntax error: unexpected select, expecting name or (Which means that if we try to add a
try,trap,assert, any keyword into the language we are running the risk of breaking a ton of code. Code that may longer be maintained.That is why I initially proposed adding a special
?go operator that can be applied to variables in the context of statements. The?character as of right now is designated as illegal character for variable names. Which means that it is not currently in use in any current Go code and therefore we can introduce it without incurring any breaking changes.Now the issue of using it in the left side of an assignment is that it doesn’t take into consideration that Go allows for multiple return arguments.
For example consider this function
if we use ? or try in the lhs of assignment to get rid of the if err != nil blocks, do we automatically presume that errors mean all other values are now trash? What if we did it like this
what assumptions do we make here? That it shouldn’t be harmful to just assume it’s okay to throw away the value? what if the error is meant to be more of a warning and the value x is fine? What if the only function that throws the err is the call to GetZ() and the x, y values are actually good? Do we presume to return them. What if we don’t use named return args? What if the return args are reference types like a map or a channel, Should we presume it’s safe to return nil to the caller?
TLDR; adding ? or
tryto assignments in an effort to eliminateintroduces way too much confusion than perks.
And adding something like the
trapsuggestion introduces the possibility for breakage.Which is why in my proposal that I made in a separate issue. I allowed for the ability to declare a
func ?() boolon any type so that when you called sayyou can have that trap side effect happen in a way that applies to any type.
And applying the ? to work only on statements like I showed allows for the programability of statements. In my proposal I suggest allowing for a special switch statement that allows for somebody to switch over cases that are the keyword + ?
If we are using ? on a type that doesn’t have an explicit ? function declared or a builtin type, then the default behavior of checking if var == nil || zero’d value {execute the statement} is the presumed intent.
@pdk
I see this leading to a lot of bugs using the flag package where people will write
if myflag { ... }but mean to writeif *myflag { ... }and it won’t be caught by the compiler.Very interesting discussion here.
I would like to keep the error variable on the left side so no magically appearing variables are introduced. Like the original proposal I would like the handling of the error stuff on the same line. I wouldn’t use the
||-operator as it looks “too boolean” for me and somehow hides the “return”.So I would make it more readable by using the extended keyword “return?”. In C# the question mark is used at some places to make shortcuts. E. g. instead of writing:
you can just write:
foo?.Bar();So for Go 2 I would like to propose this solution:
@KamyarM please be polite. If you’d like to read more about some of thinking behind keeping the language small, I recommend https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html.
I’d like to revisit the “return the error/with additional context” part again, since I assume ignoring the error is already covered by the already existing
_.I’m proposing a two-word keyword that may be followed by an string (optionally). The reason it is a two-word keyword is twofold. First, unlike an operator which is inherently cryptic, it’s easier to grasp what it does without too much prior knowledge. I’ve picked “or bubble”, because I’m hoping that the word
orwith an absence of an assigned error will signify to the user that the error is being handled here, if it is not nil. Some users will already associate theorwith handling a falsy value from other languages (perl, python), and readingdata := Foo() or ...might subconsciously tell them thatdatais unusable if theorpart of the statement is reached. Second, thebubblekeyword while being relatively short, might signify to the user that something is going up (the stack). Theupword might also be suitable, though I’m not sure whether the wholeor upis understandable enough. Finally, the whole thing is a keyword, first and foremost because its more readable, and second because that behavior cannot be written by a function itself (you might be able to call panic to escape the function you are in, but then you can’t stop yourself, someone else will have to recover).The following is only for error propagation, therefore may only be used in functions that return an error, and the zero values of any other return arguments:
For returning an error without modifying it in any way:
For returning an error with an additional message:
And finally, introduce a global adaptor mechanism for customized error modification:
Finally, for the few places where really complex handling is necessary, the already existing verbose way is already the best way.
Folks, please keep the conversation respectful and on-topic. This issue is about handling Go errors.
https://golang.org/conduct
And what about who’s coming from C/C++, Objective-C where we have the same exact problem with boilerplate? And it’s frustrating to see a modern language like Go suffer from exactly the same problems. That’s why this whole hype around errors as values feels so fake and silly - it’s already been done for years, tens of years. It feels like Go learned nothing from that experience. Especially looking at Swift/Rust who’s actually trying to find a better way. Go settled with existing solution like Java/C# settled with exceptions but at least those are much older languages.
If your language has terrible features, someone will eventually use that feature. If your language has no such feature, there is a 0% chance. It is simple math.
If they introduce the Try Catch in this language all these problems will be solved in a very easy way.
They should introduce something like this. If the value of error is set to something other than nil it can break the current workflow and automatically trigger the catch section and then the finally section and the current libraries can work too with no change. Problem solved!
Untrue.
@ardhitama Isn’t this then like Try catch except “With” is like “Try” statement and that “Else” is like"Catch"? Why not go implementing exception handling like Java or C# ? right now in go if a programmer doesn’t want to handle exception in that function, it returns it as a result of that function. Still there is no way to force a programmers to handle an exception if they don’t want to and many times you really don’t need to, but what we get here are lots of if err!=nil statements that makes the code ugly and not readable(lots of noise). Isn’t it the reason Try Catch Finally statement was invented in the first place in other programming language?
So, I think it is better if Go Authors “Try” to not be stubborn!! and just introduce the “Try Catch Finally” statement in the next versions. Thank you.
@urandom
The purpose of the block is twofold:
3 lines vs. 1 is not even a language change: if number of lines is your biggest concern, we could address that with a simple change to
gofmt.We already have
returnandpanic; addingraiseon top of those seems like it adds too many ways to exit a function for too little gain.errors.ReplaceIfNil(err, err2)would require some very unusual pass-by-reference semantics. You could passerrby pointer instead, I suppose:but it still seems very odd to me. Does the
ortoken construct an expression, a statement, or something else? (A more concrete proposal would help.)This is a bit more radical, but maybe a macro-driven approach would work better.
try!would eat the last value in the tuple and return it if not nil, and otherwise return the rest of the tuple.I hope you don’t mind me hijacking your proposal with an alternate syntax - how do folks feel about something like this:
@urandom: Under the hood it is implemented as a more costly, but single defer. If the original code:
Other overheads:
e.Must(err, msg("oh noes!")) costs about 30ns with Go 1.8. With tip (1.9), however, though still an allocation, I clocked the cost at 2ns. Of course for pre-declared error messages the cost is still negligible.(*) all numbers running on my 2016 MacBook Pro.
All in all, the cost seems acceptable if your original code uses defer. If not, Austin is working on significantly reducing the cost of defer, so the cost may even go down over time.
Anyway, the point of this package is to get experience on how using alternative error handling would feel and be useful now so we can build the best language addition in Go 2. Case in point is the current discussion, it focusses too much on reducing a few lines for simple cases, whereas there is much more to gain and arguably other points are more important.
@bcmills thanks for providing a motivation and sorry I missed it in your earlier post.
Given that premise, however, wouldn’t it be better to provide a more general facility for “clearer scoping of assignments” that could be used by all variables? I’ve unintentionally shadowed my share of non-error variables as well, certainly.
I remember when the current behavior of
:=was introduced—a lot of that thread on go nuts† was clamor for a way to explicitly annotate which names to be reused instead of the implicit “reuse only if that variable exists in exactly the current scope” which is where all the subtle hard-to-see problems manifest, in my experience.† I cannot find that thread does anyone have a link?
There are lots of things that I think could be improved about Go but the behavior of
:=always struck me as the one serious mistake. Maybe revisiting the behavior of:=is the way to solve the root problem or at least to reduce the need for other more extreme changes?I agree error handling is not ergonomic. Namely, when you read below code you have to vocalize it to
if error not nil then-which translates toif there is an error then.I would like to have abbility of expresing above code in such way -which in my opinion is more readable.
Just my humble suggestion 😃
@bcmills:
Not just the scoping; there is also the left edge. I think that really affects readability. I think
is much clearer than
especially when it occurs on multiple consecutive lines, because your eye can scan the left edge to ignore error handling.
@ALTree Alberto, what about mixing your
checkand what @ianlancetaylor proposed?so
becomes
Also, we can limit
checkto only deal with error types, so if you need multiple return values, they need to be named and assigned, so it assigns “in-place” somehow and behaves like simple “return”If
returnwould become acceptable in expression one day, thencheckis not needed, or becomes a standard functionthe last solution feels like Perl though 😄
The problem I have with most of these suggestions is it’s not clear what’s going on. In the opening post it is suggested to re-use
||for returning an error. It isn’t clear to me that a return is happening there. I think that if new syntax is going to be invented, it needs to align with most people’s expectations. When I see||I do not expect a return, or even a break in execution. It’s jarring to me.I do like Go’s “errors are values” sentiment, but I also agree that
if err := expression; err != nil { return err }is too verbose, mainly because almost every call is expected to return an error. This means you’re going to have a lot of these, and it’s easy to mess it up, depending on where err is declared (or shadowed). It has happened with our code.Since Go does not use try/catch and uses panic/defer for “exceptional” circumstances, we may have an opportunity to reuse the try and/or catch keywords for shortening error handling without crashing the program.
Here is a thought I had:
The thought is you prefix
erron the LHS with the keywordtry. If err is non-nil a return happens immediately. You do not have to use a catch here, unless the return isn’t completely satisfied. This aligns more with people’s expectations of “try breaks execution”, but instead of crashing the program it just returns.If the return is not completely satisfied (compile time check) or we want to wrap the error, we could use catch as special forward-only label like this:
This also allows you to check and ignore certain errors:
Perhaps you could even have
tryspecify the label:I realize my code examples kind of suck (foo/bar, ack). But hopefully I’ve illustrated what I mean by going with/against existing expectations. I would also be totally fine with keeping errors the way they are in Go 1. But if a new syntax is invented, there needs to be careful thought about how that syntax is already perceived, not just in Go. It’s hard to invent a new syntax without it already meaning something, so it’s often better to go with existing expectations rather than against them.
@urandom, you might avoid some magic by leaving the LHS the same as today and changing
or ... returntoreturnif (cond):This improves generality and transparency of the error values on the left and the triggering condition on the right.
@creker I think you can cover even more cases than just returning a modified error:
@dc0d The extra function calls will probably be inlined by the compiler
@urandom IMO it hides too much.
I’d like to build on the proposal from @thejerf just a little bit.
First, instead of a
!, anoroperator is introduced, that shift’s the last returned argument from the function call on the left side, and invokes a return statement on the right, whose expression is a function that is called, if the shifted argument is non-zero (non-nill for error types), passing it that argument. Its fine if people think should be only for error types as well, though I feel that this construct will be useful for functions that return a boolean as their last argument as well (is something ok/not ok).The Read method will look like so:
I’m assuming the errors package provides convenience functions like the following:
This looks perfectly readable, while covering all necessary points, and it doesn’t look too foreign as well, due to the familiarity of the return statement.
Please don’t make this way more complex than it is now. Really moving the same codes in one line(instead of 3 or more) is not really a solution. I personally don’t see any of these proposals viable that much. Guys, the math is very simple. Either embrace the “Try-catch” idea or keep the things the way they are now which means lots of "if then else"s and code noises and is not really suitable for using in OO patterns like Fluent Interface.
Thank you very much for all your hand-downs and perhaps few hand-ups 😉 (just kidding)
@mortdeus, the left side of the Dart arrow is a function signature, whereas
syscall.Chdir(dir)is an expression. They seem more or less unrelated.The more I read all the proposals (including mine) the less I think we really have a problem with error handling in go.
What I would like is some enforcement to not accidentally ignore an error return value but enforce at least
_ := returnsError()I know there is tooling to find these issues, but a first level support of the language could catch some bugs. Not handling an error at all is like having an unused variable for me - which already is an error. It would also help with refactoring, when you introduce an error return type to a function, since you are forced to handle it in all places.
The main issue that most people try to solve here seems to be the “amount of typing” or “number of lines”. I would agree with any syntax that reduces the number of lines, but that’s mostly a gofmt issue. Just allow inline “single line scopes” and we are good.
Another suggestion to save some typing is implicit
nilchecking like with booleans:or even
That would work with all pointer types of cause.
My feeling is that everything that reduces the function call + error handling into a single line will lead to less readable code and more complex syntax.
I have a feeling someone’s going to ask me about the difference between sugar and magic. (I’m asking that myself.)
Sugar is a bit of syntax that shortens code without fundamentally altering the rules of the language. The short assignment operator
:=is sugar. So is C’s ternary operator?:.Magic is a more violent disruption to the language, like introducing a variable into a scope without declaring it, or performing a non-local control transfer.
The line is definitely blurry.
I’ll throw my 2c in, and hope it’s not literally repeating something in the other N hundred comments (or stepping on the discussion of urandom’s proposal).
I like the original idea that was posted, but with two primary tweaks:
Syntactic bikeshedding: I strongly believe that anything that has implicit control flow should be an operator on its own, rather than an overload of an existing operator. I’ll throw
?!out there, but I’m happy with whatever isn’t easily confused with an existing operator in Go.The RHS of this operator should take a function, rather than an expression with an arbitrarily injected value. This would let devs write pretty terse error-handling code, while still being clear about their intent, and flexible with what they can do, e.g.
The RHS should never be evaluated if an error doesn’t happen, so this code won’t allocate any closures or whatever on the happy path.
It’s also pretty straightforward to “overload” this pattern to work in more interesting cases. I have three examples in mind.
First, we could have the
returnbe conditional if the RHS is afunc(error) (error, bool), like so (if we allow this, I think we should use a distinct operator from the unconditional returns. I’ll use??, but my “I don’t care as long as it’s distinct” statement still applies):Alternatively, we could accept RHS functions that have return types matching that of the outer function, like so:
And finally, if we really want, we can generalize this to work with more than just errors by changing the argument type:
…Which I would personally find really useful when I have to do something terrible, like hand-decoding a
map[string]interface{}.To be clear, I’m primarily showing the extensions as examples. I’m not sure which of them (if any) strike a good balance between simplicity, clarity, and general usefulness.
@KamyarM If you worked for a company that had picked Rust for its programming language, would you go to the Rust Github and start demanding garbage-collected memory management and C+±style pointers so you don’t have to deal with the borrow checker?
The reason Go programmers don’t want Java-style exceptions has nothing to do with lack of familiarity with them. I first encountered exceptions in 1988 via Lisp, and I’m sure there are other people in this thread who encountered them even earlier – the idea goes back to the early 1970s.
The same is even more true of ternary expressions. Read up on Go’s history – Ken Thompson, one of Go’s creators, implemented the ternary operator in the B language (predecessor of C) at Bell Labs in 1969. I think it’s safe to say he was aware of its benefits and pitfalls when considering whether to include it in Go.
@KamyarM The language and its idioms go together. When we consider changes to error-handling, we’re not just talking about changing the syntax, but (potentially) also the very structure of the code.
Try-catch-finally would be a very invasive such change: it would fundamentally change the way that Go programs are structured. In contrast, most of the other proposals you see here are local to each function: errors are still values returned explicitly, control flow avoids non-local jumps, etc.
To use your example of a ternary operator: yes, you can fake one today using a map, but I hope you won’t actually find that in production code. It doesn’t follow the idioms. Instead, you’ll usually see something more like:
It’s not that we don’t want to borrow syntax, it’s that we have to consider how it would fit in with the rest of the language and the rest of the code that already exists today.
I’m not much of a fan of a global adaptor. If my package sets a custom processor, and yours also sets a custom processor, whose wins?
While true, I think it’s a dangerous road to take. Looking negligible at first it would pile up and eventually cause bottlenecks later down the road when it’s already late. I think we should keep performance in mind from the beginning and try to find better solutions. Already mentioned Swift and Rust do have error propagation but implement it as, basically, simple returns wrapped into syntactic sugar. Yes, it’s easy to reuse existing solution but I would prefer to leave everything as is than to simplify and encourage people to use panics hidden behind unfamiliar syntactic sugar that tries to hide the fact that it’s, basically, exceptions.
I think you’re mistaken. People will reach for your shorthand operator because it’s so convenient, and then they’ll end up using panic a lot more than before.
Whether panics are useful sometimes or rarely, and whether they are useful across or within API boundaries, are red herrings. There are a lot of actions one might take on an error. We’re looking for a way to shorten error-handling code without privileging one action over the others.
Benchmarks? What is a “compute resource” exactly?
No, it doesn’t, because non-nil errors aren’t overwritten
My impression is you don’t understand the approach. It is logically equivalent to regular error check in a self-contained type, I suggest you study the example closely so maybe it can be the worst thing you understand rather than just the worst thing you’ve read.
@carlmjohnson
If we want one liner handling for simple error, we could change the syntax to allow the return statement to be the beggining of a one line block. It would allow people to write:
I think the spec should be changed to something like (this could be quite naive 😃)
@KamyarM Thanks for the clarification. We explicitly considered and rejected exceptions. Errors are not exceptional; they happen for all sorts of completely normal reasons. https://blog.golang.org/errors-are-values
Java exceptions are a train wreck, so I have to firmly disagree with you here @KamyarM. Just because something is familiar, does not mean that it is a good choice.
@erwbgy I did went through all issues specified by @ianlancetaylor They all relay on adding new keywords (like
try()) or using special non-alfanumeric chars. Personally - I don’t like that, as code overloaded with !"#$%& tends to look offensive, like swearing.I do agree, and feel, what the first few lines of this issue state: too much Go code goes on error handling. Suggestion I made is in line with that sentiment, with suggestion very close to what Go feels now, without need for extra keywords or key-chars.
@creker I agree that if
err != nil return erris too much boilerplate, the thing we are afraid is that if we’ll create easy way to just forward error up the stack - statistically programmers, especially juniors will use the easiest method, rather doing what is appropriate in a certain situation. It’s the same idea with Go’s error handling - it forces you to do a decent thing. So in this proposal we want to encourage others to handle and wrap errors thoughtfully.I would say that we should make simple error handling look ugly and long to implement, but graceful error handling with wrapping or stack traces look nice and easy to do.
I would suggest simpler solution:
For multiple multiple function returns:
And for multiple return arguments:
^ sign means return if parameter is not nil. Basically “move error up the stack if it happens”
Any drawbacks of this method?
The entire point of my proposal is to allow adding context or manipulating the errors, in a way I consider more programmatically correct than most of the proposals here which involve repeating that context over and over, which itself inhibits the desire to put the additional context in.
To rewrite the original example,
comes out as
except that this example is too trivial for, well, any of these proposals, actually, and I’d say in this case we’d just leave the original function alone.
Personally I don’t consider the original Chdir function a problem in the first place. I’m tuning this specifically to address the case where a function is noisied up by lengthy repeated error handling, not for a one-error function. I’d also say that if you have a function where you are literally doing something different for every possible use case, that the right answer is probably to keep writing what we’ve already got. However, I suspect that is by far the rare case for most people, on the grounds that if that was the common case, there wouldn’t be a complaint in the first place. The noise of checking the error is only significant precisely because people want to do “mostly the same thing” over and over in a function.
I also suspect most of what people want would be met with
If we shave away the problem of trying to make a single if statement look good on the grounds that it’s too small to care about, and shave away the problem of a function that is truly doing something unique for every error return on the grounds that A: that’s actually a fairly rare case and B: in that case, the boilerplate overhead is actually not so significant vs. the complexity of the unique handling code, the maybe the problem can be reduced to something that has a solution.
I also really want to see
or some equivalent be possible, for when that works.
And, of all the proposals on the page… doesn’t this still look like Go? It looks more like Go than current Go does.
Thinking about my proposal a little more, I am dropping the bit about union/sum-types. The syntax I’m proposing is
In the case of an expression, the expression is evaluated and if the result is not equal to
truefor boolean expressions or the blank value for other expressions, BLOCK is executed. In an assignment, the last assigned value is evaluated for!= true/!= nil. Following a guard statement, any assignments made will be in scope (it doesn’t create a new block scope [except maybe for the last variable?]).In Swift, the BLOCK for
guardstatements must contain one ofreturn,break,continue, orthrow. I haven’t decided if I like that or not. It does seem to add some value because a reader knows from the wordguardwhat will follow.Does anyone follow Swift well enough to say if
guardis well regarded by that community?Examples:
The solution
=?proposed by @bcmills and @jba isn’t only for error, the concept is for non zero. this example will work normally.The main idea of this proposal is the side notes, separate the primary purpose of the code and put secundary cases aside, in order to make reading easier. For me the reading of a Go code, in some cases, is not continuous, many times you have the stop the idea with
if err!= nil {return err}, so the idea of side notes seems interesting to me, as in a book we read the main idea continually and then read the side notes. (@jba talk) In very rare situations the error is the primary purpose of a function, maybe in a recovery. Normally when we have an error, we add some context, log and return, in these cases, side notes can make your code more readable. I don’t know if it’s the best syntax, particularly I do not like the block in the second part, a side note needs to be small, a line should be enough@mpvl My main concern about
errdis with the Handler interface: it seems to encourage functional-style pipelines of callbacks, but my experience with callback / continuation style code (both in imperative languages like Go and C++ and in functional languages like ML and Haskell) is that it is often much harder to follow than the equivalent sequential / imperative style, which also happens to align with the rest of Go idioms.Do you envision
Handler-style chains as part of the API, or is yourHandlera stand-in for some other syntax you are considering (such as something operating onBlocks?)To add to @ianlancetaylor’s requirements: simplifying error messages should not just make things shorter, but also easier to error handling right. Passing panics and downstream errors up to defer functions is tricky to get right.
Consider for example, writing to a Google Cloud Storage file, where we want to abort writing the file on any error:
The subtleties of this code include:
Using package errd, this code looks like:
errd.Discardis an error handler. Error handlers can also be used to wrap, log, whatever errors.e.Mustis the equivalent offoo() || wrapErrore.Deferis extra and deals with passing errors to defers.Using generics, this piece of code could look something like:
If we standardize on the methods to use for Defer, it could even look like:
Where DeferClose picks either Close or CloseWithError. Not saying this is better, but just showing the possibilities.
Anyway, I gave a presentation at an Amsterdam meetup last week on this topic and it seemed that the ability to make it easier to do error handling right is considered more useful than making it shorter.
A solution that improves on errors should focus at least as much on making it easier to get things right than on making things shorter.
My variant:
@Azareal
I think there is value in shortening it. However, I don’t think it should apply to nil in all situations. Perhaps there could be a new interface like this:
Then any value that implements this interface can be used as you proposed.
This would work as long as the error implemented the interface:
But this would not work:
Not to mention that one liners like those are much harder to read.
if != ; { }is too much even in several lines, hence this proposal. The pattern is fixed for almost all cases and can be turned into syntactic sugar that easy to read and understand.Another thing I think would be useful is if the logic in
catch {}required abreakkeyword to break the control flow. Sometimes you want to handle an error without breaking the control flow. (ex: “for each item of these data do something, add a non-nil error to a list and continue”)@urandom one way is to use the Go switch and find the type of exception in the catch. Lets say you have pathError exception you know that is caused by OpenFile() that way. Another way that is not much different from the current if err!=nil error handling in GoLang is this:
So you have options this way but you are not limited. If you really want to know exactly which line caused issue you put every single line in a try catch the same way you right now write lots of if-then-elses. If the error is not important for you and you want to pass it to the caller method which in the examples discussed in this thread are actually about that I think my proposed code just does the job.
@thejerf nice but happy path is heavily altered. many messages ago there were suggestion to have kinda Ruby like “or” sintax -
f := OpenFile(filename) or failed("couldn't open file").Additional concern - is that for any type of params or for errors only? if for errors only - then type error must have special meaning to compiler.
Part of the reason for that suggestion is because the current approach in Go doesn’t help tools to understand if a returned
errorvalue denotes a function failing or it’s returning an error value as part of its function (e.g.github.com/pkg/errors).I believe enabling functions to express failure explicitly is the first step to improving error handling.
Or just let
fmtformat the one liner return-only functions on one line instead of three:I don’t feel like I explained my idea very well, so I created a gist (and revised many of the shortcomings that I just noticed)
https://gist.github.com/KernelDeimos/384aabd36e1789efe8cbce3c17ffa390
There’s more than one idea in this gist, so I hope they can be discussed separate from each other
I believe that putting the error handling in the assignment syntax doesn’t solve the root of the problem, which is “error handling is repetitive”.
Using
if (err != nil) { return nil }(or similar) after many lines of code (where it makes sense to do so) goes against the DRY (don’t repeat yourself) principal. I believe that’s why we don’t like this.There are also problems with
try ... catch. You don’t need to explicitly handle the error in the same function where it happens. I believe that’s a notable reason why we don’t liketry...catch.I don’t believe these are mutually exclusive; we can have a sort of
try...catchwithout athrows.Another thing I personally dislike about
try...catchis the arbitrary necessity of thetrykeyword. There’s no reason you can’tcatchafter any scope limiter, as far as a working grammar is concerned. (somebody call it out if I’m wrong about this)This is what I propose:
?as the placeholder for a returned error, where_would be used to ignore itcatchas in my example below,error?could be used instead for full backwards compatibility^ If my assumption that these are backwards-compatible is incorrect please call it out.
I thought of an argument against this: if you write it this way, changing that
catchblock might have unintended affects on code in deeper scopes. This is that same problem we have withtry...catch.I think if you can only do this in the scope of a single function, the risk is manageable - possibly the same as the current risk of forgetting to change a line of error-handling code when you intend to change many of them. I see this as the same difference between the consequences of code-reuse, and the consequences of not following DRY (i.e. no free lunch, as they say)
Edit: I forgot to specify an important behaviour for my example. In the case that
?is used in a scope without anycatch, I think this should be a compiler error (rather than throwing a panic, which was admittedly the first thing I thought of)Edit 2: Crazy idea: maybe the
catchblock just wouldn’t affect the control flow… it would literally be like copying and pasting the code insidecatch { ... }to the line after the error is?ed (well not quite - it would still have its own scope). It seems weird since none of us are used to it, socatchdefinitely shouldn’t be the keyword if it’s done this way, but otherwise… why not?I suggest Go add default err handling in Go version 2.
If user does not handle error, compiler return err if it is not nil, so if user write:
compile transform it to:
If user handle the err or ignore it using _, compiler will do nothing:
or
for multiple return values, it’s similar:
compiler will transform to:
If Func()'s signature doesn’t return error but call functions which return error, compiler will report Error: “Please handle your error in Func()” Then user can just log the err in Func()
And if user want to wrap some info to the err:
or
The benefit is,
x, y, err := baz(); return (x? && y?) || err?
becomes
x, y, err := baz(); if ((x != nil && y !=nil) || err !=nil)){ return x,y, err }
Adding a mini experience report related to point 3 of @ianlancetaylor original post, return the error with additional contextual information.
When developing a flac library for Go, we wanted to add contextual information to errors using @davecheney pkg/errors package (https://github.com/mewkiz/flac/issues/22). More specifically, we wrap errors returned using errors.WithStack which annotates errors with stack trace information.
Since the error is annotated, it needs to create a new underlying type to store this additional information, in the case of errors.WithStack, the type is errors.withStack.
Now, to retrieve the original error, the convention is to use errors.Cause. This lets you compare the original error against for instance
io.EOF.A user of the library may then write something along the lines of https://github.com/mewkiz/flac/blob/0884ed715ef801ce2ce0c262d1e674fdda6c3d94/cmd/flac2wav/flac2wav.go#L78 using
errors.Causeto check the original error value:This works well in almost all cases.
When refactoring our error handling to make consistent use of pkg/errors throughout for added context information, we ran into a rather serious issue though. To validate zero-padding we’ve implemented an io.Reader which simply checks if the read bytes are zero, and reports an error otherwise. The problem is that having performed an automatic refactoring to add contextual information to our errors, suddenly our test cases started failing.
The problem was that the underlying type of the error returned by
zeros.Readnow is errors.withStack, rather than io.EOF. Thus subsequently caused issues when we used that reader in combination withio.Copy, which checks forio.EOFspecifically, and does not know to useerrors.Causeto “unwrap” an error annotated with contextual information. Since we cannot update the standard library, the solution was to return the error without annotated information (https://github.com/mewkiz/flac/commit/6805a34d854d57b12f72fd74304ac296fd0c07be).While loosing the annotated information for interfaces which return concrete values is a loss, it is possible to live with.
The take away from our experience has been that we were lucky, as our test cases caught this. The compiler did not produce any errors, since the
zerostype still implements theio.Readerinterface. We also did not think that we would hit an issue, as the added error annotation was a machine generated rewrite, simply add contextual information to errors should not affect the program behaviour in a normal state.But it did, and for this reason, we wish to contribute our experience report for consideration; when thinking of how to integrate addition of contextual information into error handling for Go 2, such that error comparison (as used in interface contracts) still holds seamlessly.
Kindly, Robin
I think that there readability of try-catch blocks in Java/C#/… is very good as you can follow the “happy path” sequence without any interruption by the error handling. The downside is that you basically have a hidden goto mechanism.
In Go I start to insert empty lines after the error handler to make the continuation of the “happy path” logic more visible. So out of this sample from golang.org (9 lines)
i often do that (11 lines)
Now back to the proposal, as I already posted something like this would be nice (3 lines)
Now I see the happy path clearly. My eyes are still aware that on the right side there is code for error handling but I only need to “eye-parse” it when really necessary.
A question to all on the side: should this code compile?
IMHO the user should be forced to say that he intentionally ignores the error with:
@robert-wallis , you have an example:
On the first use of
guard, it looks an awful lot likeif err != nil { return &FooError{"Couldn't foo fails.txt", err}}, so I’m not sure if that’s a big win.On the second use, it’s not immediately clear where the
errcomes from. It almost looks like it’s what’s returned fromos.Open, which I’m guessing was not your intent? Would this be more accurate?In which case, it looks like …
@gobwas from readability point of view it’s very important to completely understand control flow. Looking at your example there’s no telling, which line might cause a jump to the catch block. It’s like a hidden
gotostatement. No wonder modern languages try to be explicit about it and require programmer to explicitly mark places where control flow might diverge due to an error being thrown. Pretty much likereturnorgotobut with much nicer syntax.One more. If we have
trybe something that can happen on assignment lines, it goes down to 47 lines.Using
trywheretryis just a shortcut for if != nil return reduces the code by 6 lines out of 59, which is about 10%.Notably, in several places I wanted to write
try x()but I couldn’t since I needed err to be set for the defers to work properly.Back on the topic at hand, it seems like there’s a ton of proposals already made, and the odds of someone else solving this with another proposal that makes everyone go “Yes! That’s it!” approach zero.
It seems to me perhaps the conversation should move in the direction of categorizing the various dimensions of the proposals made here, and getting a sense of the prioritization. I’d especially like to see this with an eye towards revealing contradictory requirements being vaguely applied by people here.
For instance, I’ve seen some complaints about additional jumps being added in the control flow. But for myself, in the parlance of the very original proposal, I value not having to add
|| &PathError{"chdir", dir, err}eight times within a function if they are common. (I know Go is not as allergic to repeated code as some other languages, but still, repeated code is at a very high risk for divergence bugs.) But pretty much by definition, if there is a mechanism for factoring out such error handling, the code can’t flow from top-to-bottom, left-to-right, with no jumps. Which is generally considered more important? I suspect careful examination of the requirements people are implicitly placing on the code would reveal other mutually-contradictory requirements.But just in general, I feel like if the community could agree on the requirements after all this analysis, the correct solution may very well just fall out of them clearly, or at the very least, the correct solution set will be so obviously constrained that the problem becomes tractable.
(I’d also point out that as this is a proposal, the current behavior should in general be subjected to the same analysis as the new proposals. The goal is significant improvement, not perfection; rejecting two or three significant improvements because none of them are perfect is a path to paralysis. All proposals are backwards compatible anyhow, so in those cases where the current approach is already the best anyhow (imho, the case where every error is handled legitimately differently, which in my experience is rare but happens), the current approach will still be available.)
@KamyarM
Hiding cyclomatic complexity makes it harder to see but it doesn’t remove it (remember
strlen?). Just as making error handling “shortened” makes the error handling semantics easier to ignore–but harder to see.Any statements or expressions in the source that reroutes flow control should be obvious and terse, but if it’s a decision between obvious or terse, the obvious should be preferred in this case.
There is a difference between new and beneficial. Do you believe that because you have an idea, the very existence of it merits approval? As an exercise, please look at the issue tracker and try to imagine Go today if every single idea was approved regardless of what the community thought.
Perhaps you believe that your idea is better than the others. That’s where the discussion comes in. Instead of degenerating the conversation to talking about how the entire system is broken because of idioms, address criticisms directly, point-by-point, or find a middle ground between you and your peers.
@ianlancetaylor Did I use any vulgar words or language? Did I use discrimination language or any bullying of someone or any unwelcome sexual advances? I don’t think so. Come on man, We are just talking about Go programming language here. It is not about a religion or politics or anything like that. I was frank. I wanted my voice to be heard. I think that’s why there is this forum. For voices to be heard. You may not like my suggestion or my criticism. That’s OK. But I guess you need to let me talk and discuss otherwise we can all conclude that everything is perfect and there is no problem and so no needs for further discussions.
@josharian Thank you for the article I will take a look at that.
That’s might be good when you first encounter it. But reading and writing it again and again - it seems too verbose and takes too much space to convey a pretty simple thing - unhandled error with bubble up the stack. Operators are cryptic at first but they’re concise and have a good contrast with all the other code. They clearly separate main logic from error handling because it is in fact a separator. Having so much words in one line will hurt readability in my opinion. At least merge them into
orbubbleor drop one of them. I don’t see a point of having two keywords there. It turns Go into a spoken language and we know how that goes (VB, for example)There really is no fundamental difference between a panic in Go and an exception in Java or Python etc. apart from syntax and lack of an object hierarchy (which makes sense because Go doesn’t have inheritance). How they work and how they are used is the same.
Of course panics have a legitimate place in the language. Panics are for handling errors that should only occur to due to programmer error that are otherwise unrecoverable. For example, if you divide by zero in an integer context, there is no possible return value and it’s your own fault for not checking for zero first, so it panics. Similarly, if you read a slice out of bounds, try use nil as a value, etc. Those things are caused by programmer error—not by an anticipated condition, like the network being down or a file having bad permissions—so they just panic and blow up the stack. Go provides some helper functions that panic like template.Must because it’s anticipated that those will be used with hardcoded strings where any error would have to be caused by programmer error. Out of memory is not a programmer fault per se, but it’s also unrecoverable and can happen anywhere, so it’s not an error but a panic.
People also sometimes use panics as a way of shortcircuiting the stack, but that’s generally frowned on for readability and performance reasons, and I don’t see any chance of Go changing to encourage its use.
Go panics and Java’s unchecked exceptions are pretty much identical and they exist for the same reasons and to handle the same usecases. Don’t encourage people to use panics for other cases because those cases have the same problems as exceptions in other languages.
Sorry, I’m going to add my own proposal to the pile. I’ve read through most of what is here, and while I do like some of the proposals, I feel they’re trying to do too much. The issue is error boilerplate. My proposal is simply to eliminate that boilerplate at the syntax level, and leave the ways errors are passed around alone.
Proposal
Reduce error boilerplate by enabling the use of the
_!token as syntactic sugar to cause a panic when assigned a non-nilerrorvaluecould become
and
could become
Of course, the particular symbol is up for debate. I also considered
_^,_*,@, and others. I chose_!as the de-facto suggestion because I thought it would be the most familiar at a glance.Syntactically,
_!(or the chosen token) would be a symbol of typeerroravailable in the scope in which it is used. It starts out asnil, and any time it is assigned to, anilcheck is performed. If it is set to a non-nilerrorvalue, a panic is started. Because_!(or, again, the token chosen) would not a syntactically valid identifier in go, name collision wouldn’t be a concern. This ethereal variable would only be introduced in scopes where it is used, similar to named return values. If a syntactically valid identifier is needed, perhaps a placeholder could be used that would be re-written to a unique name at compile time.Justification
One of the more common criticisms I see leveled at go is the verbosity of error handling. Errors at API boundaries aren’t a bad thing. Having to get the errors to the API boundaries can be a pain though, especially for deeply recursive algorithms. To get around the added verbosity error propagation introduces to recursive code, panics can be used. I feel that this is a pretty commonly used technique. I’ve used it in my own code, and I’ve seen it used in the wild, including in go’s parser. Sometimes, you’ve done validation elsewhere in your program and are expecting an error to be nil. If a non-nil error were to be received, this would violate your invariant. When an invariant is violated, it’s acceptable to panic. In complex initialization code, sometimes it makes sense to turn errors into panics and recover them to be returned somewhere with more knowledge of the context. In all of these scenarios, there’s an opportunity to reduce error boilerplate.
I realize that it is go’s philosophy to avoid panics as much as possible. They are not a tool for error propagation across API boundaries. However, they are a feature of the language and have legitimate use cases, such as those described above. Panics are a fantastic way to simplify error propagation in private code, and a simplification of the syntax would go a long way to make code cleaner and, arguably, clearer. I feel that it is easier to recognize
_!(or@, or `_^, etc…) at a glance than the “if-error-panic” form. A token can dramatically decrease the amount of code that must be written/read to convey/understand:As with any syntax feature, there’s the potential for abuse. In this case, the go community already has a set of best practices for dealing with panics. Since this syntax addition is syntactic sugar for panic, that set of best practices can be applied to its use.
In addition to simplification of the acceptable use cases for panic, this also makes fast prototyping in go easier. If I have an idea I want to jot down in code, and just want errors to crash the program while I toy around, I could make use of this syntax addition rather than the “if-error-panic” form. If I can express myself in less lines in the early stages of development allows me to get my ideas into code faster. Once I have a complete idea in code, I go back and refactor my code to return errors at appropriate boundaries. I wouldn’t leave free panics in production code, but they can be a powerful development tool.
Scanner approach is one of the worst things that I read in context of this whole “errors are values” mantra:
I don’t think that special casing return is really better than just changing gofmt to make simple if err checks one line instead of three.
And huge Go traces with hundreds of goroutines are somehow more useful? I don’t understand where you’re going with this. Java and Go are exactly the same here. And occasionally you do find it useful to observe full stack to understand how your code ended up where it crashed. C# and Go traces helped me multiple times with that.
I read it, nothing changed. In my experience it’s not a problem. That’s what documentation is for in both languages (
net.ParseIPfor example). If you forget to check if you value is nil/null or not you have the exact same problem in both languages. In most cases Go will return an error and C# will throw an exception so you don’t even need to worry about nil. Good API doesn’t just return you null without throwing an exception or something to tell what’s wrong. In other cases you check for it explicitly. The most common types of errors with null in my experience are when you have protocol buffers where each field is a pointer/object or you have internal logic where class/struct fields could be nil depending on internal state and you forget to check for it before access. That’s the most common pattern for me and nothing in Go significantly alleviates this problem. I can name two things that do help a bit - useful empty values and value types. But it’s more about ease of programming because you’re not required to construct every variable before use.That’s a problem, I never said otherwise but people here are so fixated on Java/C#/C++ exceptions that they ignore anything that slightly resembles them. Exactly why Swift requires you to mark functions with
throwsso that you can see exactly what you should expect from a function and where control flow might break and in Rust you use ? to explicitly propagate an error with various helper methods to give it more context. They both use the same concept of errors as values but wrap it in syntactic sugar to reduce boilerplate.Null dereference was a bad example. If you don’t catch it Go and Java work exactly the same - you get a crash with stack trace. How can stack trace be useless I don’t now. You know the exact place where it happened. In both C# and Go for me it usually trivial to fix that kind of crash because null dereference in my experience is due to a simple programmer error. In this particular case there’s nothing to learn from anyone.
@lpar
That’s accidental and I didn’t see any reason in your comment that Java somehow worse at nil/null than Go. I observed numerous nil dereference crashes in Go code. They’re exactly the same as null dereference in C#/Java. You might happen to be using more value types in Go which helps (C# also has them) but doesn’t change anything.
As for exceptions, let’s look at Swift. You have a keyword
throwsfor functions that could throw an error. Function without it can’t throw. Implementation wise it works like return - probably some register is reserved for returning error and every time you throw function returns normally but carries with it an error value. So the problem of unexpected errors solved. You know exactly which function might throw, you know exact place where it could happen. Errors are values and don’t require stack unwinding. They’re just returned until you catch it.Or something similar to Rust where you have special Result type that carries result and an error. Errors can be propagated without any explicit conditional statements. Plus a ton of pattern matching goodness but that’s probably not for Go.
Both of these languages take both solutions (C and Java) and combine them to something better. Error propagation from exceptions + error values and obvious code flow from C + no ugly boilerplate code that does nothing useful. So I think it’s wise to look at these particular implementation and not turn them away completely just because they resemble exceptions in some way. There’s a reason exceptions are used in so many languages because they do have a positive side to them. Otherwise languages would ignore them. Especially after C++.
It seems Marcel is thinking along these lines:
How about a conditional defer
@erwbgy the title of this issue is proposal: Go 2: simplify error handling with || err suffix my comment was in that context. Sorry if I stepped in previous discussion.
@cznic Yup. Post conditions are not Go-way, but pre conditions also look polluted:
@mrkaspa The idea is to make code more readable. Typing code is not a problem, reading is.
@object88 imagine real production code. How much errors you expect it to generate? I would think not much. At least in a properly written application. If a goroutine is in a busy loop and constantly throws errors on each iteration, there’s something wrong with the code. But even if that’s the case, given that wast majority of Go applications are IO bound even that would not be a serious problem.
@as
I’m sorry but this is a nonsensical sentence that has nothing to do with what I said. Not gonna answer it.
Slower but how much? Does it matter? I don’t think so. Go applications are IO bound in general. Chasing around CPU cycles is silly in this case. You have much bigger problems in Go runtime which eats up CPU. It’s not an argument to throw away useful feature that helps fixing bugs.
I’m not gonna bother covering non-existent “security reasons”. But I’d like to remind you that usually application traces stored internally and only developers have access to them. And trying to hide your functions names is waste of time anyway. It’s not security. I hope I don’t need to elaborate on that.
If you insist on security reasons I would like you to think about macOS/iOS, for example. They have no problem throwing panics and crash dumps that contain stacks of all threads and values of all CPU registers. Don’t see them being affected by these “security reasons”.
Could you be any more subjective? “thoughtless error propagation strategies” where did you see that?
Again, by how much?
At this point it seems I’m talking with anyone but a programmer. Tracing benefits any and all software. It’s a common technique in all languages and all types of software that helps fixing bugs. You can read wikipedia if you would like more information on that.
I have been a fan of railway oriented programming, the idea come from Elixir’s
withstatement.elseblock will be executed oncee == nilshort circuited.Here is my proposal with pseudo code ahead:
Perhaps a two-part solution?
Define
tryas “peel off the rightmost value in the return tuple; if it is not the zero value for its type, return it as the rightmost value of this function with the others set to zero”. This makes the common caseand enables chaining
(Optionally, make it non-nil rather than non-zero, for efficiency.) If the erroring functions return a non-nil/non-zero, the function “aborts with a value”. The rightmost value of the
try’ed function must be assignable to the rightmost value of the calling function or it is a compile-time type checking error. (So this is not hard-coded to handling onlyerror, though perhaps the community should discourage its use for any other “clever” code.)Then, allow try returns to be caught with a defer-like keyword, either:
or, more verbosely perhaps but more inline with how Go already works:
In the
catchcase, the function’s parameter must exactly match the value being returned. If multiple functions are provided, the value will pass through all of them in reverse order. You can of course put a value in that resolves to a function of the correct type. In the case of thedefer-based example, if onedeferfunc callsset_catchthe next defer func will get that as its value ofcatch(). (If you are silly enough to set it back to nil in the process, you will get a confusing return value out. Don’t do that.) The value passed to set_catch must be assignable to the returned type. In both cases I expect this to work likedeferin that it is a statement, not a declaration, and will only apply to code after the statement executes.I tend to prefer the defer-based solution from a simplicity perspective (basically no new concepts introduced there, it’s a second type of
recover()rather than a new thing), but acknowledge it may have some performance issues. Having a separate catch keyword could allow for more efficiency by being easier to entirely skip when a normal return occurs, and if one wished to go for maximum efficiency, perhaps tie them to scopes so that only one is allowed to be active per scope or function, which would, I think, be nearly zero-cost. (Possibly the source code file name and line number should be returned from the catch function, too? It’s cheap at compile time to do that and would dodge some of the reasons people call for a full stack trace right now.)Either would also allow repetitive error handling to be effectively handled in one place within a function, and allow error handling to be offered as a library function easily, which is IMHO one of the worst aspects of the current case, per rsc’s comments above; the laboriousness of error handling tends to encourage “return err” rather than correct handling. I know I struggle with that a lot myself.
Wow. A lot of ideas are making code readability even worse. I’m ok with approach
Only one thing is really annoying is scoping of returned variables. In this case you have to use
valbut it’s in scope ofif. So you have to useelsebut linter will be against it (and me too), and only way isWould be nice to have access to variables out of
ifblock:return 0, wrapError("f failed", err) if err != nilcan be writtenif err != nil { return 0, wrapError("f failed", err) }if err != nil return 0, wrapError("f failed", err)can be written the same.Maybe all that’s needed here is for gofmt to leave leave
if’s written on a single line on a single line instead of expanding them to three lines?Some notes:
An interesting “experience report” from one of designers of Midori at Microsoft on the error models.
I think some ideas from this document and Swift can apply beautifully to Go2.
Introducing a new reseved
throwskeyword, functions can be defined like:Trying to call this function from another, non-throwing function will result in compilation error, because of unhandled throwable error. Instead we should be able to propagate error, which everyone agree is a common case, or handle it.
For cases when we know that a method will not fail, or in tests, we can introduce
try!similar to swift.Not sure about these though (similar to swift):
ps1. multiple return values
func ReadRune() (ch Rune, size int) throws { ... }ps2. we can return withreturn try Get()orreturn try! Get()ps3. we can now chain calls likebuffer.NewBuffer(try Get())orbuffer.NewBuffer(try! Get())ps4. Not sure about annotations (easy way to writeerrors.Wrap(err, "context")) ps5. these are actually exceptions ps6. biggest win is compile time errors for ignored exceptions@billyh
Please be more concrete: “opaque and unusual” are awfully subjective. Can you give some examples of code where you think the proposal would be confusing?
IMO that’s a feature. If someone sees an unusual operator, I suspect they’re more inclined to look up what it does instead of just assuming something that may or may not be accurate.
It does?
Read the proposal carefully:
=?performs assignments before evaluating theBlock, so it could be used for that case too:And as @nigeltao noted, you can always use the existing 'n, err := r.Read(buf)` pattern. Adding a feature to help with scoping and boilerplate for the common case does not imply that we must use it for uncommon cases too.
See the numerous issues (and their examples) that Ian linked in the original post. See also https://github.com/golang/go/wiki/ExperienceReports#error-handling.
If you’ve had specific insight from those reports, please do share it.
Along the lines of what Cznic says, it would be nice to have a solution that is useful for more than just error handling.
One way to make error handling more general is to think of it in terms of union-types/sum-types and unwrapping. Swift and Rust both have solutions with ? ! syntax, although I think Rust’s has been a bit unstable.
If we don’t want to make sum-types a high level concept, we could make it just part of multiple return, the way that tuples aren’t really part of Go, but you can still do multiple return.
A stab at syntax inspired by Swift:
You could use this for other things too, like:
@billyh
I feel that the format:
f.Close() =? err { return fmt.Errorf(…, err) }is overly verbose and confusing. I personally don’t feel that the error part should be in a block. Inevitably, that would lead it to be spread out in 3 lines instead of 1. Furthermore, in the off change that you need to do more than just modify an error prior to returning it, one can just use the currentif err != nil { ... }syntax.The
=?operator is also a tad confusing. It’s not immediately obvious what’s happening there.With something like this:
file := os.Open("/some/file") or raise(err) errors.Wrap(err, "extra context")or the shorthand:file := os.Open("/some/file") or raiseand the deferred:defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2)is a bit more wordy and the choice of word could reduce the initial confusion (i.e. people might immediately associateraisewith a similar keyword from other languages like python, or just deduce that raise raises the error/last-non-default-value up the stack to the caller).It’s also a good imperative solution, which doesn’t try to solve every possible obscure error handling under the sun. By far, the largest chunk of error handling in the wild is of the above-mentioned nature. For the later, the current syntax is also there to help.
Edit: If we want to reduce the “magic” a bit, the previous examples might also look like:
file, err := os.Open("/some/file") or raise errors.Wrap(err, "extra context")file, err := os.Open("/some/file") or raise errdefer err2 := f.Close() or errors.ReplaceIfNil(err, err2)I personally think the previous examples are better, since they move the full error handling to the right, instead of splitting it as is the case here. This might be clearer though.@mpvl
BTW, this line always calls CloseWithError with a nil error value. I think you meant to write:
@mpvl I think that the trickiness of your original example is more a result of the the API you’re using there than of Go’s error handling itself.
It is an interesting example though, particularly because of the fact that you don’t want to close the file normally if you get a panic, so the normal “defer w.Close()” idiom doesn’t work.
If you didn’t need to avoid calling Close when there’s a panic, then you could do:
assuming that the semantics were changed such that calling Close after calling CloseWithError is a no-op.
I don’t think that looks so bad any more.
Even with the requirement that the file doesn’t get written with no error when there’s a panic shouldn’t be too hard to accommodate; for example by adding a Finalize function that must be called explicitly before Close.
That can’t attach the panic error message though, but decent logging could make that clearer. (the Close method could even have a recover call inside it, although I’m not sure if that’s actually a really bad idea…)
However, I think the panic-recovery aspect of this example is somewhat of a red herring in this context, as 99+% cases of error handling don’t do panic recovery.
I like the
=?/=!/:=?/:=!proposal by @bcmills / @jba better than similar proposals. It has some nice properties:=?within a=?block)It also has some properties I do not find as nice.
Composition nests. Repeated use is going to continue to indent farther and farther right. That’s not necessarily a bad thing in itself, but I would imagine that in situations with very complicated error handling that requires dealing with errors causing errors causing errors that the code to deal with it would quickly get much less clear than the current status quo. In such a situation one could use
=?for the outermost error andif err != nilin the inner errors but then has this really improved error handling in general or just in the common case? Maybe improving the common case is all that’s needed, but I don’t find it compelling, personally.It introduces falsiness into the language to gain its generality. Falsiness being defined as “is (not) the zero value” is perfectly reasonable, but
if err != nil {is better thanif err {since it is explicit, in my opinion. I would expect to see contortions in the wild that try to use=?/etc. over more natural control flow to try to get access to its falsiness. That would certainly be unidiomatic and frowned upon, but it would happen. While potential abuse of a feature is not in itself an argument against a feature, it is something to consider.The improved scoping (for the variants that declare their parameter) is nice in some cases, but if scoping needs to be fixed, fix scoping in general.
The “only rightmost result” semantics make sense but seems a little strange to me. That’s more of a feeling than an argument.
This proposal adds brevity to the language but no additional power. It could be implemented entirely as a preprocessor that does macro expansion. That would of course be undesirable: it would complicate builds and fragment development and any such preprocessor would be extremely complicated since it has to be type-aware and hygienic. I’m not trying to dimiss by saying “just make a preprocessor”. I bring this up solely to point out that this proposal is entirely sugar. It does not let you do anything you could not do in Go now; it just lets you write it more compactly. I’m not dogmatically opposed to sugar. There is power in a carefully chosen linguistic abstraction, but the fact that it is sugar means that it should be considered 👎 until proven innocent, so to speak.
The lhs of the operators are a statement but a very limited subset of statements. Which elements to include in that subset is fairly self evident, but, if nothing else, it would require refactoring the grammar in the language spec to accommodate the change.
Would something like
be allowed?
If
is allowed that must be (somehow) equivalent to
which seems deeply problematic and likely to cause very confusing situations, even ignoring that in a very un-Go-like manner it hides the performance implications of implicitly allocating a closure. The alternative is to have
returnreturn from the implicit closure and then have an error message that you can’t return a value of typeerrorfrom afunc()which is a bit obtuse.Really though, aside from a slightly improved scoping fix, this doesn’t fix any of the problems I face dealing with errors in Go. At most typing
if err != nil { return err }is a nuisance, modulo the slight readability concerns I expressed in #21182. The two biggest problems areerrorspackage would go a long way here, though they cannot solve all the problems.I realize that those aren’t the only problems and that many find other aspects more immediately concerning, but they are the ones I spend the most time with and find more vexing and troublesome than anything else.
Better static analysis to detect when I’ve messed something up would always be appreciated, of course (and in general, not just this scenario). Language changes and conventions making it easier to analyze the source so these are more useful would also be of interest.
I just wrote a lot (A LOT! sorry!) about this but I’m not dismissing the proposal. I do think it has merit, but I am not convinced that it clears the bar or pulls its weight.
@jimmyfrasche That code is the exception, not the rule. We shouldn’t design features to make it easier to write.
Also, I question whether the
recovershould even be there. Ifw.Writeorr.Read(orio.Copy!) is panicking, it’s probably best to terminate.Without the
recover, there’s no real need for thedefer, and the bottom of the function could become@ALTree errd handles the “sophisticated error-recovery” out of the box.
@jimmyfrasche: errd does roughly what your playground example does, but also weaves in passing errors and panics to defers.
@jimmyfrasche: I agree that most proposals don’t add much to what can already be achieved in code.
@romainmenke: agree that there is too much focus on brevity. To make it easier to do things correctly should have a bigger focus.
@jba: the errd approach makes it fairly easy to scan error versus non-error flow by just looking at the left side (anything starting with e. is error or defer handling). It also makes it very easy to scan which of the return values are handled for error or defer and which not.
@bcmills: although errd doesn’t fix the scoping issues in and of itself, it eliminates the need to pass downstream errors to earlier declared error variables and all, thereby mitigating the problem considerably for error handling, AFAICT.
I’ve just checked in a package at https://github.com/mpvl/errd that addresses the issues discussed here programmatically (no language changes). The most important aspect of this package is that it not only shortens error handling, but also makes it easier to do it correctly. I give examples in the docs on how traditional idiomatic error handling is more tricky than it seems, especially in the interaction with defer.
I consider this a “burner” package, though; the aim is to get some good experience and insight how best to extend the language. It interacts quite well with generics, btw, if this would become a thing.
Still working on some more examples, but this package is ready to experiment with.
In my and @bcmills’s proposals, your eye can scan down the left side and easily take in the non-error control flow.
@jimmyfrasche
As I noted earlier, the major advantage of any of these proposals would probably need to come from clearer scoping of assignments and
errvariables: see #19727, #20148, #5634, #21114, and probably others for various ways that people have encountering scoping issues in relation to error-handling.@ianlancetaylor While I’m a fan of the overall idea, I’m not a huge supporter of the cryptic perl-like syntax for it. Perhaps a more wordy syntax would be less confusing, like:
Also, I didn’t understand whether the last argument is popped in case of assignment, E.g.:
@jba What you’re describing looks a bit like a transposed function-composition operator:
But the fact that we’re writing mostly-imperative code requires that we not use a function in the second position, because part of the point is to be able to return early.
So now I’m thinking about three observations that are all kind of related:
Error handling is like function composition, but the way we do things in Go is sort of the opposite of Haskell’s Error monad: because we’re mostly writing imperative instead of sequential code, we want to transform the error (to add context) rather than the non-error value (which we’d rather just bind to a variable).
Go functions that return
(x, y, error)usually really mean something more like a union (#19412) of(x, y) | error.In languages that unpack or pattern-match unions, the cases are separate scopes, and a lot of the trouble that we have with errors in Go is due to unexpected shadowing of redeclared variables that might be improved by separating those scopes (#21114).
So maybe what we want really is like the
=:operator, but with a sort of union-matching conditional:and perhaps a boolean version for
, okindex expressions and type assertions:Except for the scoping, that’s not much different from just changing
gofmtto be more amenable to one-liners:But the scoping is a big deal! The scoping issues are where this sort of code crosses the line from “somewhat ugly” to “subtle bugs”.
If to speak in the set of these examples:
That I would like to write in one line approximately so:
Instead of “try”, put any beautiful and suitable word.
With such a syntax, the error would return and the rest would be some default values depending on the type. If I need to return along with an error and something else specific, rather than default, then no one cancels the classic more multi-line option.
Even more shortly (without adding some kind of error handling):
) P.S. Excuse me for my English))
One thing which could be done is to make
nila falsy value and for other values to evaluate to true, so you wind up with:But I’m not sure that would really work and it only shaves off a few characters. I’ve typoed many things, but I don’t think I’ve ever typoed
!= nil.Having played with the example code a little, I’m now against the
try(label) namevariant. I think if you have multiple fancy things to do, just use the current system ofif err != nil { ... }. If you’re doing basically the same thing, such as setting a custom error message, you could do this:If anyone has used Ruby, this looks a lot like their
rescuesyntax, which I think reads reasonably well.@billyh I don’t see the need to make it more verbose, as I don’t see anything confusing from that little bit of magic in the
or. I assume that unlike @as , a lot of people don’t find anything confusing in the way we work with http handlers either.@randall77 What you suggest is more in line as a code style suggestion, and that’s where go is highly opinionated. It might not play well in the community as a whole for there suddenly to be 2 styles of formatting if statements.
@randall77 At what point will that block be line wrapped?
The solution above is more agreeable when compared to the alternatives proposed here, but Im not convinced that its better than taking no action. Gofmt should be as deterministic as possible.
@object88 That’s the idea behind the
or returnoperator. It expects a function which it will call in the event of a non-zero last returned argument from the left side. In this regard it acts exactly the same as an http.Handler, leaving the actual logic of how to handle the argument and its return (or the request and its response, in the case of a handler), to the callback. And in order to use your own custom data in the callback, you create a wrapper function that receives that data as parameters and return what is expected.Or in more familiar terms, it is similar to what we usually do with handlers:
@dc0d I’m not talking about exceptions. By re-throwing I mean returning error to the caller. The proposed
or return errors.Contextf("opening file %s", filename)basically wraps and re-throws an error.@dc0d
In that case you wouldn’t just re-throw the error but check it actual contents. For me this issue is about covering the most popular case. That is, re-throwing error with added context. Only in more rare cases do you convert error to some concrete type and check what it actually says. And for that current error handling syntax is perfectly fine and will not go anywhere even if one of the proposals here would get accepted.
My 2 cents here, I would like to propose a simple rule:
“implicit result error parameter for functions”
For any function, an error parameter is implied at the end of the result parameter list if not explicitly defined.
Assume we have a function defined as follows for the sake of discussion:
func f() (int) {} which is identical to: func f() (int, error) {} according to our implicit result error rule.
for assignment, you can bubble up, ignore, or catch the error as follows:
x := f()
if f returns error, the current function will immediately return with the error (or create a new error stack?) if the current function is main, the program will halt.
It is equivalent to the following code snippet:
x, err := f() if err != nil { return …, err }
x, _ := f()
a blank identifier at the end of the assignment expression list to explicitly signal discarding of the error.
x, err := f()
err must be handled as usual.
I believe this idiomatic code convention change should only require minimal changes in the compiler or a preprocessor should do the job.
There is a rather big difference between exceptions and errors:
Many languages treat both as exceptions.
Between generics and better error handling, I would choose better error handling since most code clutter in Go comes from error handling. While it can be said this kind of verbosity is good and is in favor of simplicity, IMO it also obscures the happy path of a workflow to the level of being ambiguous.
@lukescott have a read of this blog post by @robpike https://blog.golang.org/errors-are-values
FWIW, an old thought on simplifying error handling (apologies if this is nonsense):
The raise identifier, denoted by the caret symbol ^, may be used as one of the operands on the left hand side of an assignment. For the purposes of the assignment, the raise identifier is an alias for the last return value of the containing function, whether or not the value has a name. After the assignment completes, the function tests the last return value against its type’s zero value (nil, 0, false, “”). If it is considered zero, the function continues to execute, otherwise it returns.
The primary purpose of the raise identifier is to concisely propagate errors from called functions back to the caller in a given context without hiding the fact that this is occurring.
As an example, consider the following code:
This is roughly equivalent to:
The program is malformed if:
I think it’s to tell it what variable to keep an eye on, allowing it to be decoupled from the
errortype. It probably would require a change to shadowing rules, unless you just needed people to be really careful with it. I’m not sure about the declaration in thecatchblock, though.@KamyarM I see where you’re coming from now. I would say if there are so many people against try…catch, I see that as evidence that try…catch isn’t perfect and has faults. It’s an easy solution to a problem, but if Go2 can make error handling better than what we’ve seen in other languages I think that would be really cool.
I think it’s possible to take what’s good about try…catch without taking what’s bad about try…catch, which I proposed earlier. I agree that turning three lines into 1 or 2 solves nothing.
The fundamental problem, as I see it, is the error-handling code in a function gets repeated if part of the logic is “return to the caller”. If you want to change the logic at any point, you have to change every instance of
if err != nil { return nil }.That said, I do really like the idea of
try...catchas long as functions can’tthrowanything implicitly.@KernelDeimos I agree with you but I see many comments on this thread which was essentially advocating the old with moving exact 4 5 lines into one single line which I don’t see it as really solution and also many in Go Community very RELIGIOUSLY reject using Try-Catch which close the doors for any other opinions. I personally think those who invented this try-catch concept really thought about it and although it might have few flaws but those flaws are just caused by bad programming habits and there is no way to force programmers to write good codes even if you remove or limit all the good or some may say bad features a programming language may have. I proposed something like this before and this is not exactly java or C# try-catch and I think it can support current error handling and libraries and I use one of the examples from above. So basically the compiler checks for the errors after each line and jump to the catch block if err value is set to not nil:
@KamyarM IMO, “use the best known alternative or make no changes at all” is not a very productive statement. It halts innovation and facilitates circular arguments.
I don’t like the idea of hiding return statement, I prefer:
In this case
oris a zero conditional forwarding operator, forwarding the last return if it’s non-zero. There is a similar proposal in C# using the operator?>Here’s a proposal that I think hasn’t been suggested before. Using an example:
The meaning of this is the same as this:
(where
_xyzzyis a fresh variable whose scope extends only to these two lines of code, andRmay be multiple values).Advantages to this proposal is not specific to errors, does not treat zero values specially, and it’s easy to concisely specify how to wrap errors inside any particular block of code. The syntax changes are small. Given the straightforward translation, it’s easy to understand how this feature works.
Disadvantages are that it introduces an implicit return, one can’t write a generic handler that just returns the error (since its return values need to be based on the function where it’s called from), and that the value that’s passed to the handler is not available in the calling code.
Here’s how you might use it:
Or you might use it in a test to call t.Fatal if your init code fails:
Two things:
errorvalue as failing, but that’s an assumption. What if the function returns 2errorvalues?My suggestion had something that I removed, which was automatic propagation:
I removed that since I think error handling should be explicit.
edit: Changed r.Read invocation to match
io.Reader.Read().@ibrasho , how is you example different from …
… if we give compiler warnings or lint for unhandled instances of
error? No language change required.I believe an addition to the syntax that denotes a function can fail is a good start. I propose something along these lines:
If a function fails (by using the
failkeyword instead ofreturn, it returns the zero value for each return parameter.This approach will have the benefit of allowing current code to work, while enabling a new mechanism for better error handling (at least in syntax).
Comments on the keywords:
failsmight not be the best option but it’s the best I can think of currently. I thought of usingerr(orerrs), but how they are used currently might make that a bad choice due to current expectations (erris most likely a variable name, anderrsmight be assumed to be a slice or array or errors).handlecould be misleading a bit. I wanted to userecover, but it’s used forpanics…edit: Changed r.Read invocation to match
io.Reader.Read().It occurs to me that I can get everything I want with just the addition of some operator that returns early if the right-most error is not nil, if I combine it with named parameters:
Currently my understanding of the Go consensus is against named parameters, but if the affordances of name parameters change, so can the consensus. Of course if the consensus is sufficiently strong against it, there’s an option of including accessors.
This approach gets me what I’m looking for (making it easier to handle errors systematically at the top of a function, ability to factor such code, also line count reduction) with pretty much any of the other proposals here too, including the original. And even if the Go community decides it doesn’t like it, I don’t have to care, because it’s in my code on a function-by-function basis and has no impedance mismatch in either direction.
Though I would express a preference for a proposal that permits a function of signature
func GetInt() (x int, err error)to be used in code withOtherFunc(GetInt()?, "...")(or whatever the eventual result is) to one that can’t be composed into an expression. While a lesser annoyance to the constant repetitious simple error handling clause, the amount of my code that unpacks a function of arity 2 just so it can have the first result is still annoyingly substantial, and doesn’t really add anything to the clarity of the resulting code.How about a new conditional
return(orreturnIf) statement?ie.
Isn’t this basically just trap?
Here’s a spitball:
59 lines → 44
trapmeans “run this code in stack order if a variable marked with?of specified type is not the zero value.” It’s likedeferbut it can affect control flow.But it still has less visual clutter.
if err =,; err != nil {- even if it’s a one liner there’s still too much going on for such a simple thingtry/catch is only shorter than if/else when you try multiple things back to back, which is something more or less everyone agrees is bad because of the control flow issues, etc.
FWIW, Swift’s try/catch at least resolves the visual problem of not knowing which statements might throw:
@robert-wallis: I mentioned Swift’s guard statement earlier in the thread but the page is so big Github no longer loads it by default. 😛 I still think that’s a good idea, and in general, I support looking at other languages for positive/negative examples.
I wrote this before reading carlmjohnson’s suggestion, which is similar…
Just a
#before an error.But in a real world application you would still have to write the normal
if err != nil { ... }so you can log errors, this makes the minimalistic error handling useless, Unless you could add a return middleware through annotations calledafter, which runs after the functions returns… (likedeferbut with args).cleaner than:
This is what (somewhat updated) my proposal would look like:
Definitions:
popis legal for function expressions where the right-most value is an error. It is defined as “if the error is not nil, return out of the function with the zero values for all other values in the results and that error, otherwise yield a set of values without the error”.pophas no privileged interaction witherroring(); a normal return of an error will still be visible toerroring(). This also means you can return non-zero values for the other return values, and still use the deferred error handling. The metaphor is popping the rightmost element off the “list” of return values.erroring()is defined as going up the stack to the deferred function being run, and then the previous stack element (the function the defer is running in, NewClient in this case), to access the value of the returned error currently in progress. If the function has no such parameter, either panic or return nil (whichever makes more sense). This error value need not have come frompop; it is anything that returns an error from the target function.seterr(error)allows you to change the error value being returned. It will then be the error seen by any futureerroring()calls, which as shown here allows the same defer-based chaining that can be done now.I’m using the hashicorp wrapping and multierror here; insert your own clever packages as desired.
Even with the extra function defined, the sum is shorter. I expect to amortize the two functions across additional uses, so those should only count partially.
Observe I just leave the forwardPort handling alone, rather than try to jam some more syntax around it. As an exceptional case, it’s OK for this to be more verbose.
The most interesting thing about this proposal IMHO can only be seen if you imagine trying to write this with conventional exceptions. It ends up getting fairly deeply nested, and handling the collection of the errors that can occur is quite tedious with exception handling. (Just as in real Go code the .Close errors tend to be ignored, errors that happen in the exception handlers themselves tend to be ignored in exception-based code.)
This extends existing Go patterns such as
deferand the use of errors-as-values to make easy correct error-handling patterns that in some cases are difficult to express either with current Go or exceptions, does not require radical surgery to the runtime (I don’t think), and also, doesn’t actually require a Go 2.0.Disadvantages include claiming
erroring,pop, andseterras keywords, incurring thedeferoverhead for these functionalities, the fact that factored error handling does jump around some to the handling functions, and that it doesn’t do anything to “force” correct handling. Though I’m not sure the last is possible, since by the (correct) requirement to be backwards compatible, you can always do the current thing.I hope to collect the notes at some point here myself, but I want to address what is IMHO another major stumbling block in this discussion, the triviality of the example(s).
I’ve extracted this from a real project of mine and scrubbed it for external release. (I think the stdlib is not the best source, as it is missing logging concerns, among others…)
(Let’s not bikeshed the code too much. I can’t stop you of course, but remember this isn’t my real code, and has been mangled a bit. And I can’t stop you from posting your own sample code.)
Observations:
if forwardPort == 0clause deliberately continues through errors, and yes, this is the real behavior, not something I added for this example.I wouldn’t claim this demonstrates every possible error behavior, but it does cover quite a gamut. (I often defend Go on HN and such by pointing out that by the time my code is done cooking, it is often the case in my network servers that I have all kinds of erroring behaviors; examining my own production code, from 1/3rd to fully 1/2 of the errors did something other than simply get returned.)
I will also (re-)post my own (updated) proposal as applied to this code (unless someone convinces me they’ve got something even better before then), but in the interests of not monopolizing the conversation I’m going to wait at least over the weekend. (This is less text than it appear to be because it’s a huge chunk of source, but still…)
@josharian Do you want to consider the scoping problems (https://github.com/golang/go/issues/21161#issuecomment-319277657) as a variant of the “visual clutter” problem, or a separate issue?
Well, I looked backed at my comments to see if there is anything bad there. The only thing I might have insulted(I still call that criticism btw) is GoLang programming language Idioms! Hahah!
To go back to our topic, If you hear my voice, please Go Authors consider bringing back the Try catch blocks. Leave it to the programmer to decide to use it in the right place or not (you already have something similar, I mean the panic defer recover then why not Try Catch which is more familiar to programmers?). I suggested a workaround for the current Go Error handling for backward compatibility. I am not saying that’s the best option but I think it is viable.
I will retire myself from discussing more on this topic.
Thank you for the opportunity.
@josharian I was just talking frankly there. I wanted to show the exact issues in the language or community. Consider that as more criticism. GoLang is open to criticism, is that right?
@KamyarM I use both Go and Java, and I emphatically do not want Go to copy exception handling from Java. If you want Java, use Java. And please take discussion of ternary operators to an appropriate issue, e.g. #23248.
Please leave your personal feelings out of this topic. You obviously have some problem with me and don’t want to bring anything useful, so just ignore my comments and leave a down vote as you did with pretty much every comment before. No need to keep posting these nonsensical answers.
@mccolljr Thanks, but one of the goals of this proposal is to encourage people to develop new ways to handle all three cases of error handling: ignore the error, return the error unmodified, return the error with additional contextual information. Your panic proposal does not address the third case. It’s an important one.
First of all, the readability issue is something this syntax change directly addresses:
vs
Leaving readability aside for the moment, the other reason given is performance. Yes, it’s true that using panics and defer statements incurs a performance penalty, but in many cases this difference is negligible to the operation being performed. Disk & network IO are going to, on average, take much longer than any potential stack magic for managing defers/panics.
I hear this point parroted a lot when discussing panics, and I think it’s disingenuous to say that panics are a performance degradation. They certainly CAN be, but they don’t have to be. Just like many other things in a language. If you’re panicking inside a tight loop where the performance hit would really matter, you should not also be deferring within that loop. In fact, any function that itself chooses to panic should generally not catch it’s own panic. Similarly, a go function written today wouldn’t both return an error and panic. That’s unclear, silly, and not best practice. Perhaps that’s how we may be used to seeing exceptions used in Java, Python, Javascript, etc, but that’s not how panics are generally used in go code, and I don’t believe that adding an operator specifically for the case of propagating an error up the call stack via panic is going to change the way people use panic. They’re using panic anyway. The point of this syntax extension is to acknowledge the fact that developers use panic, and it has uses that are perfectly legitimate, and reduce the boilerplate around it.
Can you give me some examples of problematic code that you think this syntax feature would enable, that are not currently possible/against best practices? If someone is communicating errors to the users of their code via panic/recover, that is currently frowned upon and would obviously still continue to be, even if syntax such as this were added. If you could, please answer the following:
var1, err := trySomeTask1(); if err != nil { panic(err) }convey thatvar1, _! := trySomeTask1()does not? Why?It seems to me that the crux of your argument is that “panics are bad and we shouldn’t use them”. I can’t unpack and discuss the reasons behind that if they’re not shared.
I, like most gophers, like the idea of errors-as-values. I believe it helps clearly communicate what parts of an API guarantee a result versus which can fail, without having to look at the documentation.
It allows for things like collecting errors and augmenting errors with more information. That is all very important at API boundaries, where your code intersects with user code. However, within those API boundaries it’s often not necessary to do all of that with your errors. Especially if you’re expecting the happy path, and have other code responsible for handling the error if that path fails.
There are some times when it’s not your code’s job to handle an error. If I’m writing a library, I don’t care if the network stack is down - that’s outside of my control as a library developer. I’ll return those errors back to the user code.
Even in my own code, there are times where I write a piece of code who’s only job is to give the errors back to a parent function.
For example, say you have an http.HandlerFunc read a file from disk as the response - this will almost always work, and if it fails it’s likely either the program is not properly written (programmer error) or there’s a problem with the file system outside of the program’s scope of responsibility. Once the http.HandlerFunc panics, it exits and some base handler will catch that panic and write a 500 to the client. If at any point in the future I’d like to handle that error differently, I can replace
_!witherrand do whatever I want with the error value. The thing is, for the life of the program, I will likely not need to do that. If I’m running into issues like that, the handler’s not the part of the code responsible for handling that error.I can, and normally do, write
if err != nil { panic(err) }orif err != nil { return ..., err }in my handlers for things like IO failures, network failures, etc. When I do need to check an error, I can still do that. Most of the time, though, I’m just writingif err != nil { panic(err) }.Or, another example, If I’m recursively searching a trie (say in an http router implementation), I’ll declare a function
func (root *Node) Find(path string) (found Value, err error). That function will defer a function to recover any panics generated going down the tree. What if the program is creating malformed tries? What if some IO fails because the program isn’t running as a user with the correct permissions? These issues aren’t the problem of my trie search algorithm - unless I explicitly make it so later - but they’re possible errors I may encounter. Returning them all the way up the stack leads to a lot of extra verbosity, including holding what will ideally be several nil error values on the stack. Instead, I can choose to panic an error up to that public API function and return it to the user. At the moment, this still incurs that extra verbosity, but it doesn’t have to.Other proposals are discussing how to treat one return value as special. It’s essentially the same thought, but instead of using features already built into the language, they look to modify the language’s behavior for certain cases. In terms of ease of implementation, this type of proposal (syntactic sugar for something already supported) is going to be the easiest.
Edit to add: I’m not married to the proposal that I’ve made as it is written, but I do think that looking at the error handling problem from a new angle is important. Nobody is suggesting anything that novel, and I want to see if we can reframe our understanding of the issue. The issue is that there are too many places where errors are being explicitly handled when they don’t need to be, and developers would like a way to propagate them up the stack without extra boilerplate code. It turns out Go already has that feature, but there’s not nice syntax for it. This is a discussion of wrapping existing functionality in a less verbose syntax to make the langauge more ergonimic without changing the behavior. Isn’t that a win, if we can accomplish it?
@carlmjohnson One of two things has to be true:
I suspect the answer is 1. I also disagree that “panics are just exceptions by another name”. I think that kind of hand-waving prevents real discussion. There are key differences between panics and exceptions as seen in most other languages.
I understand the knee-jerk “panics are bad” reaction, but personal feelings about use of panic does not change the fact that panics are used, and are in fact useful. The go compiler uses panics to bail out of deeply recursive processes in both the parser and in the type-checking phase (last I looked). Using them to propagate errors through deeply recursive code seems not only to be an acceptable use, but a use endorsed by the go developers.
Panics communicate something specific:
There are always going to be places in code where that is true. Especially early on in development. Go’s been modified to improve the refactoring experience before: the addition of type aliases. Being able to propagate unwanted errors with panic until you can flesh out if and how to handle them at a level closer to the source can make writing and progressively refactoring code much less verbose.
I feel like most proposals here are proposing large changes to the language. This is the most transparent approach I could come up with. It allows the entire current cognitive model of error handling in go to remain intact, while enabling syntax reduction for a specific, but common, case. Best practices currently dictate that “go code shouldn’t panic across API boundaries”. If I have public methods in a package, they should return errors if something goes wrong, except on rare occasions where the error is unrecoverable (invariant violations, for example). This addition to the language does would not supersede that best practice. This is simply a way to reduce boilerplate in internal code, and make sketching ideas more clear. It certainly makes code easier to read linearly.
is a lot more readable than
@urandom
Error coalescing beyond one boxable type and its actions shouldn’t be encouraged. To me it indicates a lack of effort to wrap or add error context between errors originating from different unrelated actions.
@as
The scanner type works because it is doing all the work, therefore can afford to keep track of its own error along the way. Lets not kid ourselves that this is a universal solution, please.
Another bikeshed:
trymeans “return if not the empty value”. In this I’m assumingerrors.Errorfwill returnnilwhen err is nil. I think this is about as much savings as we can expect while sticking with the goal of easy wrapping.@nick-korsakov the original proposal (this issue) wants to add more context to errors:
See also this comment.
In this comment I suggest that to make progress in this discussion (rather than running in loops) we should define better the goals e.g. what is a carefully handled error message. The whole thing is pretty interesting to read but it seems affected by a three-second goldfish memory problem (not much focused/moving forward, repeating nice creative syntax changes and arguments about exceptions/panics etc).
That’s my argument also - we want to rethrow errors in most cases, that’s usually the default case. And maybe give it some context. With exceptions the context is added automatically by stack traces. With errors like in Go we do it by hand by adding error message. Why not make it more simple. And that’s exactly what other languages are trying to do while balancing it with the issue of clarity.
So I agree with “Let’s rethrow non-nil error automatically if it was not assigned to a variable (without any extra syntax)” but the latter part bugs me. That’s where the root of the problem with exceptions and why, I think, people are so against talking about anything slightly related to them. They change control flow without any extra syntax. That’s a bad thing.
If you look at Swift, for example, this code will not compile
acan throw an error so you have to writetry a()to even propagate an error. If you removethrowsfrombthen it will not compile even withtry a(). You have to handle the error insideb. That’s a much better way of handling errors that solves both the issue of unclear control flow of exceptions and verbosity of Objective-C errors. The latter being pretty much exactly like errors in Go and what Swift is meant to replace. What I don’t like istry, catchstuff that Swift also uses. I would much prefer to leave errors as part of a return value.So what I would propose is to actually have the extra syntax. So that the call site tells by itself that it’s a potential place where control flow might change. What I would also propose is that not writing this extra syntax would produce a compile error. That, unlike how Go works now, would force you to handle the error. You could throw in the ability to just silent the error with something like
_because in some cases it would be very frustrating to handle every little error. Like,printf. I don’t care if it fails to log something. Go already has these annoying imports. But that solved with tooling at least.There’re two alternatives to compile time error I can think of right now. Like Go now, let the error be silently ignored. I don’t like that and that was always my problem with Go error handling. It doesn’t force anything, default behavior is to silently ignore the error. That’s bad, that’s not how you write robust and easy to debug programs. I had too many cases in Objective-C when I was lazy or out of time and ignored the error just to be hit with a bug in that same code but without any diagnostic info as to why it happened. At least logging it would allow me to solve the problem right there in many cases.
The downside is that people may start ignoring errors, place
try, catch(...)everywhere so to speak. That’s a possibility but, at the same time, with errors ignored by default it’s even easier to do so. I think argument about exceptions doesn’t apply here. With exceptions, what some people are trying to achieve is an illusion of their program being more stable. The fact that unhandled exception crashes the program is the problem here.Other alternative would be to panic. But that’s just frustrating and brings memory of exceptions. That definitely would lead people to do “defensive” coding so that their program doesn’t crash. For me modern language should do as much stuff as possible at compile time and leave as few decisions to runtime as possible. Where panic might be appropriate is at top of the call stack. For example, not handling an error in the main function would automatically produce a panic. Does this also apply to goroutines? Probably shouldn’t.
My 2 cents to error handling (sorry if such idea was mentioned above).
We want to rethrow errors in most cases. This leads to such snippets:
Let’s rethrow non-nil error automatically if it was not assigned to a variable (without any extra syntax). The above code will become:
Compiler will save
errto implicit (invisible) variable, check it forniland proceed (if err == nil) or return it (if err != nil) and return nil at the end of function if no errors occured during the function execution as usually but automatically and implicitly.If
errmust be handled it have to be assigned to an explicit variable and used:Error may be suppressed such a way:
In rare (fantastic) cases with more than one errors returned, explicit error handling will be mandatory (like now):
If you want to pass errors back to the caller to handle, that’s fine. The point is that it’s visible that you’re doing so, at the point where the error was returned to you.
Consider:
From looking at the code, you know that an error might occur when calling the function. You know that if the error occurs, code flow will end up at point A. You know that the only other place code flow will end up is point B. There’s also an implicit contract that if err is nil, x is some valid value, zero or otherwise.
Contrast with Java:
From looking at the code, you have no idea whether an error might occur when the function is called. If an error occurs and is expressed as an exception, then a
gotohappens, and you could end up somewhere at the bottom of the surrounding code… or if the exception isn’t caught, you could end up somewhere at the bottom of a completely different piece of your code. Or you could end up in someone else’s code. In fact, since the default exception handler can be replaced, you could potentially end up literally anywhere, based on something done by someone else’s code.Furthermore, there’s no implicit contract that x is valid – it’s common for functions to return null to indicate errors or missing values.
With Java, these problems can occur with every single call – not only with bad code, it’s something you have to worry about with all Java code. That’s why Java development environments have pop-up help to show you whether the function you’re pointing at might cause an exception or not, and which exceptions it might cause. It’s why Java added checked exceptions, so that for common errors you had to at least have some warning that the function call might raise an exception and divert program flow. Meanwhile, the returned nulls and the unchecked nature of
NullPointerExceptionare such a problem that they added theOptionalclass to Java 8 to try and ameliorate it, even though the cost is having to explicitly wrap the return value on every single function that returns an object.My experience is that
NullPointerExceptionfrom an unexpected null value I’ve been handed is the single most common way my Java code ends up crashing, and I usually end up with a big backtrace which is almost entirely useless, and an error message which doesn’t indicate the cause because it was generated far away from the code at fault. In Go, I honestly haven’t found nil dereference panics to be a significant problem, even though I’m much less experienced with Go. That, to me, indicates that Java should learn from Go, rather than the other way around.I don’t think anyone is saying that the syntax is the problem with Java-style exceptions.
What is different if someone just pass the error to the previous caller in GoLang and that caller just return that to previous caller and so on so forth (other than lots of code noise)? How much codes we need to look and traverse to see who is going to handle the error? The same goes with try-catch.
What can stop the programmer? Sometimes the function really doesn’t need to handle an error. we just want to pass the error to the UI so a user or the system admin can resolve it or find a workaround.
If a function doesn’t want to handle an exception it simply doesn’t use try-catch block so the previous caller can handle it. I don’t think the syntax has any issue. It is also much cleaner. Performance and the way it is implemented in a language is different though.
As you can see below , we need to add 4 lines of code just not to handle an error:
While, for and if/else don’t involve flow of execution jumping invisibly to somewhere else with no marker to indicate that it will.
People coming from C/C++ exactly praise Go for NOT having exceptions and for making a wise choice, resisting those who claim it is “modern” and thanking god about readable workflow (especially after C++).
On Tue, 17 Apr 2018 at 03:46, Antonenko Artem notifications@github.com wrote:
@ShalokShalom Not much. But isn’t that just an state machine? In case of failure do this and in case of success to that? Well I think not all type of errors should handle like exceptions. When only a user input validation is needed one can just simply return a boolean value with the detail on the validation error(s). Exceptions should be limited to IO or Network access or bad function inputs and in the case an error is really critical and you want to stop the happy execution path at all costs.
One of the reason some people say Try-Catch is not good is because of its performance. Probably that is caused by using a handler map table for each place that an exception may occur. I read somewhere that even the exceptions are faster(Zero-Cost when no exceptions occur but has way more cost when they actually happen) comparing it to If Error check(It is always checked regardless of having error or not). Other than that I don’t think there is any issue with Try-Catch syntax. It is only the way it is implemented by the compiler that makes it different not its syntax.
@as I don’t agree with you that try-catch is a terrible feature. It is very useful feature and makes our life much easier and that’s the reason we are commenting here so may be Google GoLang team adds a similar functionality. I personally hate those if-elses error handling codes in GoLang and I don’t like that defer-panic-recover concept that much (It is similar to try-catch but not as organized as it is with Try-Catch-Finally blocks). It adds so much noise into the code that makes the code unreadable in many cases.
@urandom Exceptions in Java are not bad. The problem is with bad programmers that don’t know how to use it.
@ianlancetaylor I just referred to the Try-Catch in other programming languages such as C++, Java , C# ,… and not the solution I had here. It was better if GoLang had the Try-Catch from day 1 so we didn’t need to deal with this way of error handling (which was not actually new . You can write the same GoLang error handling with any other programming language if you want to code like that) but what I suggest was a way to have backward compatibility with the current libraries that can return error object.
@KamyarM You seem to be suggesting adding a mechanism to throw an exception whenever a variable is set to a non-zero value. That is not the “paradigm that everyone are familiar with.” I’m not aware of any language that works that way.
@sbinet This is better than nothing but if they just simply use the same try-catch paradigm that everyone are familiar with it is much better.
That would significantly change the meaning of
defer– it’s just something that’s run at the end of the scope, not something that causes a scope to exit early.@dmajkic There is more to the proposal than just the title - ianlancetaylor describes three ways of handling errors and specifically points out that few proposals make it easier to return the error with additional information.
I am found of simpler version. Needs just one
if !errNothing special, intuitive, no extra punctuation, vastly smaller code@as Totally agree that most proposed features are impractical, and I’m starting to think that || and ? stuff could be the case.
@creker copy() and append() are not trivial tasks to implement
I have linters on CI/CD and they literally force me to handle all errors. They are not a part of language, but it don’t care - I just need results. (and by the way, i have strong opinion - if somebody’s not using linters in Go - he’s just … )
About screen size - it’s not even funny, seriously. Please stop this irrelevant discussion. Your screen could be as wide as you want - you’ll always have a probability that
|| return &PathError{Err:err}part of code won’t be visible. Just google word “ide” and see what kind of space is available for code.And please read other’s text thoughtfully, I did not say that Go forces you to handle all errors
@as Totally agree that
less runes != simple.By simple i mean readable and understandable. So that anyone who is not familiar with go should read it and understand what it does. It should be like a joke - you don’t have to explain it.
Current error handling actually is understandable, but not completely readable if you have too much
if err != nil return.The @bcmills proposal fits better.
Regarding your named return example, do you mean…
I would propose using a new keyword “returnif” which the name reveals instantly its function. Also it is flexible enough that it could to be used in more use cases than error handling.
Example 1 (using named return):
a, err = something(b) if err != nil { return }
Would become:
a, err = something(b) returnif err != nil
Example 2 (Not using named return):
a, err := something(b) if err != nil { return a, err }
Would become:
a, err := something(b) returnif err != nil { a, err }
@gdm85
Exactly what I meant by stack traces and chained error messages being similar. They both record a path that it took to the error. Only in case of messages you could end up with completely useless messages that could be from anywhere in the program if you’re not careful enough writing them. Only benefit of chained errors is the ability to record values of variables. And even that could be automated in case of function arguments or even variables in general and would, at least for me, cover almost everything I need from errors. They would still be values, you can still type switch them if you need. But at some point you would probably log them and being able to see stack trace is extremely useful.
Just look at what Go does with panics. You get full stack trace of every goroutine. I can’t remember how many times it helped me nail down the cause of the error and fix it in no time. It often amazed me how easy it is. It flows perfectly with the whole language being very predictable that you don’t even need debugger.
There seems to be stigma around everything related to Java and people often don’t bring any arguments. It’s bad just because. I no fan of Java but that kind of reasoning is not helping anybody.
No one is making that argument. Let’s not confuse what’s being discussed here with exception handling where all error handling is in one place. Calling it “largely failed” is just an opinion but I don’t think Go will ever return to that in any case. Go error handling is just different and can be improved.
That’s the Java argument — move all the error code somewhere else so you don’t have to look at it. I think it’s misguided; not just because Java’s mechanism for doing so has largely failed, but because when I’m looking at code, how it will behave when there’s an error is as important to me as how it will behave when everything works.
@gladkikhartem in the real world it’s not that simple. Yes, you expect errors in a sense that function signature includes an error as its return value. But what I ment by expecting is knowning exactly why it happened and what caused it, so you actually know what to do to fix it or not to fix it at all. Bad user input is usually really simple to fix just by looking at the error message. If you use profocol buffers and some required field is not set, that’s expected and really simple to fix if you properly validate everything you receive on the wire.
At this point I no longer understand what we’re arguing about. Stack trace or chain of error messages are pretty similar if implemented propertly. They reduce search space and provide you useful context to reproduce and fix an error. What we need is to think about ways of simplifying error handling while providing enough context. I’m in no way advocating that simplicity is more important than proper context.
This works in the playground, if you don’t reformat it:
So it seems to me that the original proposal, and many of the others mentioned above, are trying to come up with a clear shorthand syntax for those 100 characters, and stop gofmt from insisting on adding linebreaks and reformatting the block across 3 lines.
So let’s imagine we change gofmt to stop insisting on multi-line blocks, start with the line above, and try to come up with ways to make it shorter and clearer.
I don’t think the part before the semicolon (the assignment) should be changed, so that leaves 69 characters we might cut down. Of those, 49 are the return statement, the values to return, and the error wrapping, and I don’t see much value in changing the syntax of that (say, by making return statements optional, which confuses users).
So that leaves finding a shorthand for
; if err != nil { _ }where underscore represents a chunk of code. I think any shorthand should explicitly includeerrfor clarity even if it makes the nil comparison somewhat invisible, so we’re left with coming up with a shorthand for; if _ != nil { _ }.Imagine for a moment that we use a single character. I’m going to pick § as a placeholder for whatever that character might be. The line of code would then be:
I don’t see how you could do much better than that without either changing existing assignment syntax or return syntax, or having invisible magic happen. (There’s still some magic, in that the fact that we’re comparing err to nil isn’t readily apparent.)
That’s 88 characters, saving a grand total of 12 characters on a line 100 characters long.
So my question is: Is it really worth doing?
Edit: I guess my point is, when people look at Go’s
if err != nilblocks and say “I wish we could get rid of that crap”, 80-90% of what they’re talking about is stuff you inherently have to do in order to handle errors. The actual overhead caused by Go’s syntax is minimal.All of those contain some kind of un-obvious magic, which does not simplify things for the reader. In the former two examples,
errbecomes some sort of pseudo keyword or spontaneously occurring variable. In the latter two examples, it’s not at all clear what that:operator is supposed to be doing – is an error going to get automatically returned? Is the RHS of the operator a single statement, or a block?@KamyarM , he didn’t say that Go is an academic language, he said it’s based on academic research. Nor was the article about Go, but it investigates the error handling paradigm employed by Go.
If you find that
manucorporat/tryworks for you, then please do use it in your code. But the costs (performance, language complexity, etc.) of addingtry/catchto the language itself are not worth the tradeoff.@gladkikhartem I disagree that a form of “automatic wrapping” would be much worse to navigate and in helping to understand what’s going wrong. I also do not get exactly what you refer to in Java stack traces (I guess of exceptions? aesthetically ugly? what specific problem?), but to discuss in a constructive direction: what could be a good definition of “carefully handled error”?
I ask both to enhance my understanding of Go best practices (the more or less canonical that they might be) and because I feel like such definition might be key to make some proposal towards an improvement from current situation.
What I have seen so far in this discussion is a combination of:
This is in line with the original issue description by @ianlancetaylor that mentions both aspects, however in my opinion the two should be discussed/defined/experimented separately and possibly in different iterations to limit the scope of changes and just for reasons of effectiveness (a bigger change to the language is more difficult to do rather than an incremental one).
1. Syntax verbosity reduction
I like the idea of @gladkikhartem, even in its original form that I report here since it was edited/extended:
In the context of a func:
This short syntax - or in the form proposed by @gladkikhartem with
err^- would address the syntax verbosity part of the problem (1).2. Error context
For the 2nd part, adding more context, we could even completely forget about it for now and later on propose to automatically add a stacktrace to each error if a special
contextErrortype is used. Such new native error type could sport full or short stacktraces (imagine aGO_CONTEXT_ERROR=full) and be compatible with theerrorinterface while offering the possibility to extract at least the function and filename from the top call stack entry.When using a
contextError, somehow Go should attach the call stacktrace at exactly the point where the error is created.Again with a func example:
Only the type changed from
errortocontextError, which could be defined as:(notice how this
Stack()is different than https://golang.org/pkg/runtime/debug/#Stack, since we would hope to have a non-bytes version of the goroutine call stack here)The
Cause()method would return nil or the previouscontextErroras a result of nesting.I am very well aware of the potential memory implications of carrying around stacks like this, hence I hinted at the possibility of having a default short stack that would contain only 1 or few more entries. A developer would usually enable full stracktraces in development/debug versions and leave the default (short stacktraces) otherwise.
Prior art:
Just food for thought.
Interesting, @buchanae , but does it get us much over:
I do see that it would allow
ato escape, whereas in the current state, it’s scoped to the then and else blocks.I’m all for simplifying the error handling in Go (although I personally don’t mind it that much), but I think this adds a bit of wizardry to an otherwise simple and extremely easy to read language.
There’s another possibility that strikes me. A lot of the friction that I experience when trying to write throwaway Go code quickly is because I have to error check on every single call, so I can’t nest calls nicely.
For example, I cannot call http.Client.Do on a new request object without first assigning the http.NewRequest result to a temporary variable, then calling Do on that.
I wonder if we could allow:
to work even if
yreturns(T, error)tuple. When y returns an error, the compiler could abort the expression evaluation and cause that error to be returned from f. If f doesn’t return an error, it could be given one.Then I could do:
and the error result would be non-nil if either NewRequest or Do failed.
This has one significant problem, however - the above expression is already valid if f accepts two arguments, or variadic arguments. Also, the exact rules for doing this are likely to be quite involved.
So in general, I don’t think I like it (I’m not keen on any of the other proposals in this thread either), but thought I’d throw the idea out for consideration anyway.
To me this proposal alone looks incomplete and has too much magic. How would be
??operator defined? “Captures last return value if non-nil”? “Captures last error value if matches method type?”Adding new operators for handling return values based on their position and type looks like a hack.
On 29 Aug 2017, 13:03 +0300, Mikael Gustavsson notifications@github.com, wrote:
I think the operator proposed by @jba and @bcmills is a very nice idea, although it reads better spelled as “??” instead of “=?” IMO.
Looking at this example:
I think doStuff2 is considerable easier and quicker to read because it:
@bcmills The hypothetical ReplaceIfNil would be a simple:
Nothing unusual about that. Maybe the name …
orwould be a binary operator, where the left operand would be either an IdentifierList, or a PrimaryExpr. In the case of the former, it is reduced to the rightmost identifier. It then allows the right-hand operand to be executed if the left one is not a default value.Which is why i needed another token afterwards, to do the magic of returning the default values, for all except the last parameter in the function Result, which would take the value of the expression afterwards. IIRC, there was another proposal not too long ago which would have the language add a ‘…’ or something, that would take the place of the tedious default value initialization. In that cause, the whole thing might look like this:
f, err := os.Open("/some/file") or return ..., errors.Wrap(err, "more context")As for the block, I understand that it allows for wider handling. I’m personally not sure whether this proposal’s scope should be to try and cater to every possible scenario, as opposed to cover a hypothetical 80%. And I personally believe that it matters how many lines a result would take (though I never said it was my biggest concern, that is in fact the readability, or lack thereof, when using obscure tokens like =?). If this new proposal spans multiple lines in the general case, I personally don’t see its benefits over something like:
ifscope. And that would still make a function with just a couple of such statements harder to read, due to the visual noise of these error handling blocks. And that is one of the complaints that people have when discussing error handling in go.I’d like to suggest that our problem statement is,
To address this problem statement, I propose these goals for error handling improvements in Go 2.x:
Evaluating this proposal:
according to those goals, I would conclude that it succeeds well on goal #1. I’m not sure how it helps with #2, but it doesn’t make adding context any less likely either (my own proposal shared this weakness on #2). It doesn’t really succeed at #3 and #4, though:
=?syntax is also unusual. It’s especially confusing if combined with the similar but different=!syntax. It will take a while for people to get used to their meanings; andMaking the error handling a block might be a good idea, though, if as others have suggested, it’s combined with changes to gofmt. Relative to my proposal, it improves generality, which should help with goal #4 and familiarity which helps goal #3 at the cost of a sacrifice in brevity for the common case of simply returning the error with added context.
If you had asked me in the abstract, I might have agreed that a more general solution would be preferable to an error handling specific solution as long as it met the error handling improvement goals above. Now though, having read this discussion and thought about it more, I’m inclined to believe an error handling specific solution will result in greater clarity and simplicity. While errors in Go are just values, error handling constitutes such a significant portion of any programming that having some specific syntax to the make error handling code clear and concise seems appropriate. I’m afraid we will make an already hard problem (coming up with a clean solution for error handling) even harder and more complicated if we conflate it with other goals such as scoping and composability.
Regardless,though, as @rsc points out in his article, Toward Go 2, neither the problem statement, the goals nor any syntax proposal is likely to advance without experience reports that demonstrate that the problem is significant. Maybe instead of debating various syntax proposals, we should start digging for supporting data?
@bcmills: ah I see what you mean now. Yes, that should work. I’m not worried about the number of lines, though, but rather about the subtlety of the code.
I find this to be in the same category of problems as the variable shadowing, just less likely to run in to (possibly making it worse.) Most variable shadowing bugs you’ll arguable run in to with good unit tests. Arbitrary panics are harder to test for.
When operating at scale it is pretty much guaranteed you’ll see bugs like this manifesting. I may be paranoid, but I’ve seen far less likely scenarios lead to data loss and corruption. Normally this is fine, but not for transaction processing (like writing gs files.)
Why not? The
return errat the end will setrerrtonil.@bcmills: this code has two problems 1) same as @rogpeppe mentioned: err passed to CloseWithError is always nil, and 2) it still doesn’t handle panics so that means the API will report success explicitly when there is a panic (the returned r might emit an io.EOF even when not all bytes have been written), even if 1 is fixed.
Otherwise I’m agree that the error returned by Close can often be ignored. Not always, though (see first example).
I do find it somewhat surprising that there were like 4 or 5 faulty suggestions were made on my rather straightforward examples (including one from myself) and I still feel like I have to argue that error handling in Go is not trivial. 😃
@bcmills Oh, I see. To open another can of bikesheds, I suppose that you could say
instead of
but WriteString would remain unchanged:
@jba:
writeToGS still terminates if there is a panic, as it should(!!!), it merely ensures it calls CloseWithError with a non-nil error. If the panic is not handled, the defer is still called, but with err == nil, resulting in a potentially corrupt file materializing on Cloud Storage. The right thing to do here is to call CloseWithError with some temporary error and then continue the panic.
I found a bunch of examples like this in Go code. Dealing with io.Pipes also often result in a little bit too subtle code. Handling errors is often not as straightforward as it seems as you saw now yourself.
@jimmyfrasche: understood, although it is good if an API doesn’t require you to do that. 😃
@mpvl I was just trying to whittle it down to a few things so it was easier to wrap my head around how it worked, how to use it, and to imagine how it would fit in with code I wrote.
@jimmyfrasche
The main improvement is in keeping the
errvariable out of scope. That would avoid bugs such as the ones linked from https://github.com/golang/go/issues/19727. To illustrate with a snippet from one of them:The bug occurs in the last if-statement: the error from
Decodeis dropped, but it’s not obvious because anerrfrom an earlier check was still in scope. In contrast, using the::or=?operator, that would be written:Here there are two scoping improvements that help:
err(from the earlierGetcall) is only in scope for thereturnblock, so it cannot be used accidentally in subsequent checks.errfromDecodeis declared in the same statement in which it is checked for nilness, there can be no skew between the declaration and the check.(1) alone would have been sufficient to reveal the error at compile time, but (2) makes it easy to avoid when using the guard statement in the obvious way.
@mpvl I’m looking at the docs and src for errd. I think I’m starting to understand how it works but it looks like it has a lot of API that gets in the way of understanding that seems as if it could be implemented in separate package. I’m sure that all makes it more useful in practice but as an illustration it adds a lot of noise.
If we ignore common helpers like top-level funcs for operating on the result of WithDefault(), and assume, for the sake of simplicity that we always used context, and ignore any decisions made for performance would the absolute minimal barebone API reduce to the below operations?
Looking at the code I see some good reasons it’s not defined as above, but I’m trying to get at the core semantics in order to better understand the concept. For example, I’m not sure if
IsSentinelis in the core or not.That depends on how you unpack them, which I suppose brings us back around to where we are today. If you prefer unions, perhaps you can envision a version of
=?as an “asymmetric pattern-matching” API:Where
matchwould be the traditional ML-style pattern match operation, but the in the case of a nonexhaustive match would return the value (as aninterface{}if the union has more than one unmatched alternative) rather than panicking with a nonexhaustive match failure.@bcmills I think I’m responsible for at least half of the words in #19412 so you don’t need to sell me on sum types 😉
When it comes to returning stuff with an error there are four cases
If you hit 4 that’s where things get tricky. Without introducing tuple types (unlabeled product types to go with struct’s labeled product types) you’d have to reduce the problem to case 3 by bundling everything up in a struct if you want to use sum types to model “this or an error”.
Introducing tuple types would cause all kinds of problems and compatibility issues and weird overlaps (is
func() (int, string, error)an implicitly defined tuple or are multiple return values a separate concept? If it’s an implicitly defined tuple then does that meanfunc() (n int, msg string, err error)is an implicitly defined struct!? If that’s a struct, how do I access the fields if I’m not in the same package!)I still think sum types provide many benefits, but they don’t do anything to fix the issues with scoping, of course. If anything they could make it worse because you could shadow the entire ‘result or error’ sum instead of just shadowing the error case when you had something in the result case.
I like the
checkproposal because you can also extend it to handleOther proposals don’t seem like they can mix with
deferas well.checkis also very readable, and simple to Google if you don’t know it. I don’t think it needs to be limited to theerrortype. Anything that’s a return parameter in the last position could use it. So, a iterator might have acheckfor aNext() bool.I once wrote a Scanner that looks like
That last bit could be
check s.read(&rt)instead.Mixing the idea @bcmills we can introduce conditional pipe forwarding operator.
The
F2function will be executed if the last value is not nil.A special case of pipe forwarding with return statement
Real example brought by @urandom in another issue For me much more readable with focus in primary flow
I agree that error handling in Go can be repetitive. I don’t mind the repetition, but too many of them affects readability. There is a reason why “Cyclomatic Complexity” (whether you believe in it or not) uses control flows as a measure for complexity. The “if” statement adds extra noise.
However, the proposed syntax “||” is not very intuitive to read, especially since the symbol is commonly known as an OR operator. In addition, how do you deal with functions that return multiple values and error?
I am just tossing some ideas here. How about instead of using error as output, we use error as input? Example: https://play.golang.org/p/rtfoCIMGAb