-
Notifications
You must be signed in to change notification settings - Fork 18k
proposal: Go 2: try
keyword for Try Calls
#68391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
This appears to be a restatement of the check proposal, without being able to handle errors, and doesn't address any of the issues that led to check/handle being declined. |
It seems to me it's not so much the check proposal, as the try proposal in #32437. This appears to be nearly identical to that proposal, but it doesn't address the reasons that that proposal was declined. In fact, this proposal even calls that out:
Yes: that is the one of the main reasons that #32437 was declined. |
@seankhliao, thank you so much for your prompt reply. As @ianlancetaylor corrected, thank you so much; the proposal aligns more with #32437. I have read that Unfortunately, your replies didn't address our proposal as a whole. The chapter that spokes about implicit control flows states, among other things:
As we well know, the language already has many implicit control-flow switches. In this proposal, we have done everything possible to ensure minimal cognitive load (disturbance). This proposal is unique because it helps only that part of the error handling that actually[1] needs help. The proposal says:
[1] Actually, most Go repos use error propagation for most error handling cases: >60%. (I was hoping that this starts to raise eyebrows.) There is a chapter in FAQ about differences to
If you have read the language spec part, you will notice that it makes a lot of sense. Of course, you cannot do these: check err
try(err) But the orthogonality in the Go language specs is undeniable with this proposal. And this is from the
In our proposal, we reached the opposite conclusion. Context sensitivity has been proven to be very valuable in practice and the opposite of dangerous. That's also a meaningful difference between our proposal and #32437. We are sorry that our proposal is so long. We still hope that someone from the Go team reads it all. Since 2018, we have tried to gather all the new knowledge. Thank you for your valuable work. |
Thanks for all the work you've put into this proposal. Still, this proposal is very similar to the rejected try proposal. We can't spend our time reconsidering rejected ideas. |
Thanks. I don't speak for our proposal anymore, but generally. IMHO, we must reconsider rejected ideas at some point. We have made mistakes, received new information, made false presumptions, etc. |
Yes: if there is relevant new information, then we will reconsider an earlier decision. |
Go2 proposal:
try
keyword forTry Calls
Would you consider yourself a Go programmer?
Experienced.
What other languages do you have experience with?
Pro in C, C++, ASM (mainly x86, but others), Scala, C#, F#, Java,
Object-C, Swift, Dart, etc.
Would this make Go easier to learn, and why?
Learning for Beginners
It depends on the individual, but it would make learning and adapting Go easier.
Not only by offering a familiar mechanism but by bringing something that's
missing—error propagation—would get you started faster.
Getting Proficient, Become a Real Expert
As highlighted by Daniel Kahneman in Thinking, Fast and Slow, expert
programmers (like chess masters) often rely on System 1 thinking—intuitive and
fast—when solving problems. Go's error-handling pattern, characterized by
repetitive
if
statements, introduces unnecessary noise, disrupting thisintuitive flow and hindering experienced programmers from leveraging their full
capabilities. Furthermore, the Dreyfus model of skill acquisition shows that
experts thrive on absorbed awareness—being deeply immersed in their work.
Supertalented and visually competent programmers who excel at skimming and
absorbing large codebases quickly, also suffer from the excessive cognitive
burden imposed by
if err != nil
. The cluttered code obstructs their ability toutilize their visual strengths, ultimately limiting their efficiency and
effectiveness.
Neurodivergent Programmers
Neurodivergent individuals often face increased cognitive load with Go's current
error-handling pattern. The repetitive and verbose error checking can be
overwhelming, leading to slower onboarding and skill acquisition. That hampers
their productivity and affects their engagement and satisfaction with the
language.
Learning of Go will be more accessible with the proposal:
faster.
learning a new programming language—how to do things better.
Has this idea, or one like it, been proposed before?
Several are somehow similar; these are the most important ones:
The proposal we present here needs features some might consider
necessary for error handling. Those can be added later
to the language, the standard library, or through 3rd party packages. We
mention this one as an example:
How does this proposal differ?
propagate errors if they happen.
in all the same contexts where function calls have been working; only the
call behavior changes accordingly.
The most crucial difference is that even the
try
keyword can execute animplicit control flow branch,
i.e., error propagation, it's not different than the language's current implicit
control-flows. We are using a keyword with well-known and straightforward semantics,
which allows us to offer clear (and orthogonal) error propagation control flow.
Who does this proposal help, and why?
Who it helps?
Before we answer the question, let's leave a couple of counter-questions
lingering in our minds:
This proposal helps:
examples why the current
error handling makes it difficult.
Cycle,
TDD, etc.
in Go.
In summary, it helps us to make better Go programs faster, and it helps everyone to
maintain those programs to their full potential.
Do you still need convincing?
Let's use a real-world example:
but it's a verified and generally recognized way of controlling flows that should be
handled.
could be twisted. Of course, it doesn't mean absolutely or always. It's
when we are skimming the code, maybe the first time.
some empathy and inclusivity would come around in the Go community. The
person behind the comment says: 'for me.' The code full of repetitive DRY
violations doesn't bother some, but others may suffer greatly.
with legacy Go code. You cannot find those critical decision points from Go
code as fast as you are used to.
Test yourself how important small things can be for skimmability:
Everybody reacts differently.—these things matter.
Why does it help?
Go's CSP is
the root cause of why it has error values. Values are data, and data is something
we can quickly move through channels. How about if errors wouldn't be simple
values to be processed and moved around but something you must first catch or
something else? Please look at the code block below to see how easy it is to use
errCh
.Errors are values, and it's a good
thing, but we have many function calls, some of which need help. We must
offer a decent way to call functions that return errors—we need
error propagation.
How much would it help? Do you have any figures?
We have measured a few well-known Go projects by running a script to get
statistics. It's much easier to understand how important the issue is when we
know the math behind it.
has been:
are structured in this study.
tested the figures from the repos presented here.
if-statement but it still just propagates the error, we don't count it.
debugging capabilities; the download is below.
We have manually checked the figures of the following two repos are correct:
kubernetes
if (!=|==) nil
variations:^\s*if.*err [!=]= nil
^\s*if.*err [!=]= nil {\n.*return.*err$
^\s*if.*err [!=]= nil {\n.*return.*\.(Fatal|Errorf|New).*err[\)]*$
^\s*if.*err != nil {\n.*panic\(.*err[\)]*$
^\s*(if|case|switch).*err == (?!nil).*{
^\s*(if|case|switch).*err == .*EOF.*{
^\s*(if|case|switch).*errors\.Is\(
^\s*(if|case|switch).*errors\.As\(
^\s*(if|case|switch).*errors\.Is\(.*EOF
^\s*panic\(
^.*recover\(\)
^\s*\btry\b
cockroach
if (!=|==) nil
variations:^\s*if.*err [!=]= nil
^\s*if.*err [!=]= nil {\n.*return.*err$
^\s*if.*err [!=]= nil {\n.*return.*\.(Fatal|Errorf|New).*err[\)]*$
^\s*if.*err != nil {\n.*panic\(.*err[\)]*$
^\s*(if|case|switch).*err == (?!nil).*{
^\s*(if|case|switch).*err == .*EOF.*{
^\s*(if|case|switch).*errors\.Is\(
^\s*(if|case|switch).*errors\.As\(
^\s*(if|case|switch).*errors\.Is\(.*EOF
^\s*panic\(
^.*recover\(\)
^\s*\btry\b
Clarifications:
Try Call
insteadof
if err != nil { return ..., err }
; we could easily refactor (simplify) themwith a script or a tool.
if err != nil ...
with
Try Call
if it clarifies error messages, or we could use deferred errorannotation helpers. Also, this can be semi-automated.
The results are positive for this proposal. Quite interestingly, K8s and Cockroach
got almost the same percentage of the cases that would be easily
(automatically) transformed to use
try
based error propagation. More than 60%of the error-handling cases would be able to be written with
try calls
inK8s. Note that we found a Go repo Dolt DB
that's score is 80% for automatic error propagation!
K8s and Cockroach ~58% interested us so much that we decided to check other
famous Go repos like hugo, and it had the
exact figure: 56.40% + 8.12% = 64.52%. According to Go's sources, 34% use error
propagation, but 24% use
panic
to transport errors. Both figures aresurprisingly high, but the
panic
score is explained by being a standardlibrary. However, the
panic
usage is much higher than community police forcesclaim.
Shocking fact is that how little all the repos do the actual error-handling,
i.e., make programmatic decisions according to the error values.
If you compare the propagation figures to figures of
errors.Is
orerrors.As
, you should wonder what all the fuss is about errorannotation.
The Emperor's New Clothes
We ran statistics through our
mod
directory, and 56% of all (2003929)error-handling cases are currently propagations, i.e., plain
if err != nil { return err }
. But the error-handling happens <1% ofcases, and most are
io.EOF
checks. Weird? No, not actually.It seems that the community has misunderstood the Go's error-handling
proverb:
'... handle them gracefully'. If you listen to every word Rob Pike says
in the error-handling part, he says 'Think about whether you should be doing
something with that error'.
Go community has invented this 'you must add context to your errors
or you aren't handling them'. There are several blog
posts
that have started to notice that something is wrong with this rule and its end
results:
failed to generate a random document ID: failed to generate random bytes: unexpected EOF`
Does Golang have the best error handling in the world?
It would be fascinating to study what Go's error-handling policies and idioms
have achieved. These figures make you wonder if Emperor Go is naked
after all. Is the situation clearly that much better than in other languages?
What is the proposed change?
A new
try
keyword (operator) will be added to the language. It's similara keyword like in
Swift
and in Zig.
Example of the language spec
We propose a new expression,
Try Calls
to the language specification. The
Try Calls
will be an extension toCalls in the specification. We assume that the
try
keyword will be a new operator for functions, e.g., receive<-
operator is for channels.
Try Call
Expressionwraps a function
f
to a new temporary (inline) functionf'
, and theninvokes a function call for the temporary function
f'
that will invoke theoriginal function
f
and evaluates to the result of thef
call with thefinal error result removed.
More formally:
turns into
The
f
must be a function or method call whose last result parameter is a valueof type
error
. Thetry
with a function whose last result parameter is not avalue of type
error
leads to a compile-time error.The
f
evaluates in a function or method call, producing n+1 result values oftypes
T1
,T2
, ...Tn
, anderror
for the last value. If the functionf
evaluates to a single value (n is 0), that value must be of type
error
andtry f()
returns no result and can only appear as an expressionstatement.
The
try call
can be used in two different types of code context depending onthe result parameters of the enclosing function. If a compiler generates the
enclosing function, it's treated like it has no result parameters.
The usage categories are:
When the
try call
is used inside a function with at least one resultparameter where the last result is of type
error
the following happens:Invoking
try
with a function callf()
as in (pseudo-code)The line turns into the following (in-lined) code:
In other words, if the last value produced by the
f()
, of typeerror
, isnil, the
try f()
simply returns the first n values, with the final nilerror stripped. If the last value produced by the
f()
is not nil, theenclosing function's error result variable (called
_err
in the pseudo-code above,but it may have any other name or be unnamed) is set to that non-nil error
value and the enclosing function returns. If the enclosing function declares
other named result parameters, those result parameters keep whatever value
they have. If the function declares other unnamed result parameters, they
assume their corresponding zero values (which is the same as keeping the
the value they already have).
When the
try call
is used inside a function whose last result parameteris not of type
error
, the following happens:Invoking
try
with a function callf()
as in (pseudo-code)The line turns into the following (in-lined) code:
This version works similarly to the previous category. Only if the last value
produced by the
f()
is not nil, the code panics with the currenterror
value. When panicking, enclosing function's result parameters arehandled the same as in the previous category.
The
try call
is an expression, and it can be used for all variable initializations,when previous category rules are fulfilled.
The example:
Is this change backward compatible?
We did a few searches and found these, which confirms that
try
is used as a name,but also that it's easy to fix with tools:
try
named variable in tests.try
named variables.When the Go version roll-out is done similarly as with the latest features: first, as
an experimental feature and then official, it gives time and tools to prepare
repos.
Show example code before and after the change.
Examples when enclosing function returns error
Example 1 - error values needed
Before:
After:
if err != nil { return err }
would wanted to replace it can be done. Seethe Orthogonality? chapter.
Example 2 - mixed error propagation
Before
After:
Example 3 - Classic copy file
Before:
After:
cp
command inUnix
Example 4 from the one possible future
Not part of the proposal
Optional - possibilities in the future.
(Existing of
_err
would help implementation ofrecovererr()
)ideas to the table, but which aren't directly related or anyhow mandatory
for the functionality of this proposal. (We suggest leaving comments on
the actual proposals.)
prominent ones would be these:
errdefer
is like the currentdefer
, but the deferred function iscalled only if the current's function's error return parameter is not nil
and the deferred functions error return value replaces the current error
return value of the function who deferred.
recovererr()
builtin function like the currentrecover()
, but insteadof returning possible panics,
recovererr
returns possible error returnvalue of the function where
defer
orerrdefer
was used. The same rulesapply as
recover
; it works only in deferred functions.recovererr
always returns an error if it's called inside an errdeferred function.
defer
-recover
rules andare orthogonal in that way, too.
Try Calls
offer a path that is easy to extend and stillfollow Go's principles
Examples when enclosing function does not return error
Example 5 usage in the
main
functionIn playground use, errors will trigger panics, and we will not stop them, which
is commonly OK in playgrounds.
To stop panics and have proper error messages, add one line from some
helper package:
Example 6 usage in tests
The panic functionality allows
Try Calls
to work as-is with the current testharness.
When using our prototype, this has been a used feature, especially at each
project's start.
What is the cost of this proposal?
How many tools would be affected?
All of them.
What is the compile time cost?
Marginally slower or the same. There are fewer code lines to process, but the
compiler must build
Try Calls
. Less code but more to do; maybe it's ±0. Itdepends on the project. (Some information might be available from languages
with similar keywords and functionality.)
What is the run time cost?
No cost on error propagation
The
Try Calls
are built during the compile time, i.e., inline wrapped. If thefunction
f
intry f()
stays in-lined similarly as without thetry
thenthere won't be any performance penalty.
In cases where
defer
is used for error handling, the error control flowis a little slower, but the happy path inside the
f
is the same. During theprototype use, the main reason some functions were slower was the lack of
inlining because of the use of
defer
. Thatmight be one reason why a new
errdefer
would be reasonable. It would bringa smaller optimization context. However, let's remember we are solving only
error propagation with this proposal.
All in all,
Try Calls
would open new opportunities to offer error and stacktraces to programmers. We have tested these with the prototype with high
success.
Can you describe a possible implementation?
Please see the example of the language spec
chapter.
Prototype
The OSS package err2 implements similar
functionality through its
try.To()
functions. The package also shows how toadd declarative error handlers with
defer
to your code.Learnings from the usage of the prototype in several Go projects which have >100
KLOC:
err2
package has taught us thatnested
try
is not desired or used. Searching thecode base where
try.ToX
has been used only two (2) times in the nested waywhere
try.ToX
has been used 1144 times.try
with a just variable (try err
) is rarely used or needed. It was used 15 times, and thetotal was 1144.
if err
statements were still used, especially with the channels. There were 80 error handling related
if
statements.
How would the language spec change?
Please see the example of the language spec
chapter. In addition to that, we presume that
try
is a low precedence operator for error-returning function calls.
Our previous definition holds when
try
is an operator andf()
is an operand.The operand that
try
operates must follow the conditions we defined inTry Call
chapter.Because the
try
'operator' has low precedence, we would need parenthesis to getthe previous code line to work:
In summary, the
try
operator must build a new expression from the given operand (afunction call) before a
Try Call
can be made, i.e., if you want to donested calls, you have to use parenthesis. We have tested this in Swift and in
Zig and they work the same.
We think this is better:
Orthogonality?
As far as we know, this is orthogonal with the current language features.
continue precisely as you would now without the
try
.Try Call
.errors, you would still perform a
Try Call
and annotate errorsin deferred helpers. There are OSS options, and the standard
library will get its own soon after proposal ratification.
For orthogonality reasons, we have decided that
try
is the operator, theoperand is a function
f()
, and the result is a function call. You cannot writetry err
. It will only compile with help. However, help is easy to arrange:Nevertheless, we think that this clarity and simplicity (especially in the
language spec) is a good thing.
try
is meant to be used with the functionreturning error value ◻︎
Is the goal of this change a performance improvement?
No, not at the moment, but it might open new doors.
Does this affect error handling?
Very much.
How does this differ from previous error handling proposals?
We don't try to solve something that's not broken, i.e., error-value-based
handling stays as it is now. According to our statistics, we bring minimalistic and orthogonal ways to
propagate errors in more than half of the current error handling cases.
.
This proposal is as simple and minimal as it can be. By using a keyword, we keep it
semantically as near as possible with the current error value-based handling.
↓ Go-intuitive mapping ↑
Our experience with other languages with similar constructs confirms this. In our
prototype, we offered a way to add a handler function to
try
, but no one used it.Is this about generics?
No.
FAQ
Q: Why is this proposal so long?
A: We think many things have drifted in the wrong direction in Go's
error handling. But at the same time, so many new results and innovations have come
to the surface that we thought it was time to
try
.Q: Why not
try()
macro?A: Simple,
try
is not related to error values. It is related to functions. Thetry
is an operator whose operand is a function.We also think that readability (skimmability) is better, and error handling is
a serious matter, let's use the convention it deserves.
Vs.
The difference is slight but meaningful for some of us. For instance, the
try
as a separate keyword aligns well withdefer
andgo
eventhough these are statements. How convenient that they both start an implicit and
separated control flow.
Last but not least, we have learned that, e.g., dyslexic persons prefer wider
stance of the text.
Q: Why not
guard
ormust
?A: We think both
guard
andmust
have different general semantics. Forexample, Swift's
guard
deals with boolean expressions and commonly needsan
else
-statement. Andmust
is more of a Go idiom for naming panic helpers. (Theseare just subjective opinions, not facts!)
Q: Does this solve the shadowing problem?
A: Not necessarily, but it moves us towards the goal. We still need to assign
the error value to a variable in cases where we really need to handle them, i.e.,
make an algorithmic decision according to the error value.
However, our experience in practice is that we'd not need to show anymore,
because the required amount of the error variables per scope will go down
drastically.
More information about the subject in handle/check
spec
and closing the issue 377.
Q: How about test coverage?
A: This can be handled similarly as other languages have done. Maybe some
instrumentation has to be used.
Q: How about debugging?
A: Debugging will get new tools, e.g., you can ask the debugger to break if an
error occurs in any
Try Call
automatically. When you set the breakpoint of theline, including the
try
operator, you can select which control flow branch itbreaks. There are limitless options here, usually when a DRY violation is fixed.
Debugger or Go runtime could offer a try-trap to where you could add, e.g.,
logging during a debugging session.
Zig language has built error tracing (not entirely around
try
), but similarly inGo, we could start to use
try
as a source for automatic error traces, whichwould be a great help in debugging.
The text was updated successfully, but these errors were encountered: