Description
Go2 proposal: try
keyword for Try 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 this
intuitive 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 to
utilize 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:
- Because it's easier to start hacking with the language: faster feedback loops.
- More skimmable code helps you to understand and learn how to use the language
faster. - Similarities with other languages are always good when learning a new one.
- Easier to refactor and maintain is undeniably essential in any phase of
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?
- We leave the current error value-based handling as it is now.
- We bring a new expression to help calls to error-returning functions and
propagate errors if they happen. - The new expression is just another version to make a function call. It works
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 an
implicit 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:
Is Go's error-handling strategy inadvertently discriminating against different
cognitive styles and preferences? Are we dismissing the needs of neurodivergent programmers and those who excel through visual and intuitive coding methods?
This proposal helps:
- Those who cannot now use their full potential to skim Go code.
- Those who want to be able to refactor Go code quickly. See these
examples why the current
error handling makes it difficult. - Everyone who is using some Incremental Hacking
Cycle,
TDD, etc. - Polyglot developers.
- Everyone who has made a copy and past mistakes with their error annotations
in Go. - Everyone who has suffered the Go program's error message stuttering.
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:
"I find those if-blocks degrade readability significantly. When I read code I
haven't written, I almost always want to see what it does in the happy path. I
usually don't care how an error is handled. Having those blocks everywhere not
only makes things more difficult for me to read, but it might hide proper if
blocks since they might be a single tree in that forest" — Anonymous,
(bold text annotations made by proposal author)
- the happy path: That's the real problem. It's both subjective per person
but it's a verified and generally recognized way of controlling flows that should be
handled. - don't care how an error is handled: This is a perfect example of how things
could be twisted. Of course, it doesn't mean absolutely or always. It's
when we are skimming the code, maybe the first time. - more difficult for me to read: We picked this here that maybe finally
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. - hide proper if blocks: We have suffered dramatically
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:
- set your tab size to 1
- remove syntax highlighting from your tools
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
.
errCh := make(chan error)
go func() {
data, err := os.ReadFile(kubeconfigPath)
t.Logf("read %d, err=%v\n", len(data), err)
ctx := ktesting.Init(t)
errCh <- run(ctx, s) // Look me mama now!
}()
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.
- The executed script
has been:ag --ignore '*_test.go' -c -G '\.go$' --no-filename --no-group '%s' | jq -s 'add'
- We have skipped test files because we're more interested in how actual algorithms
are structured in this study. - The script can have blind spots and corner cases. However, we have manually
tested the figures from the repos presented here. - Our script is conservative. For example, if code has a comment line after the
if-statement but it still just propagates the error, we don't count it. - If you are interested in these edge cases, use the script's
debugging capabilities; the download is below. - Play with yourself: full bash-script as gist
- We have skipped test files because we're more interested in how actual algorithms
We have manually checked the figures of the following two repos are correct:
Error Handling Subject | RegEx | Count | % of 1st |
---|---|---|---|
All if (!=|==) nil variations: |
^\s*if.*err [!=]= nil |
46073 | 100.00% |
Auto Propagation: | ^\s*if.*err [!=]= nil {\n.*return.*err$ |
26915 | 58.41% |
Easy Propagation: | ^\s*if.*err [!=]= nil {\n.*return.*\.(Fatal|Errorf|New).*err[\)]*$ |
3369 | 7.31% |
Panic Propagation: | ^\s*if.*err != nil {\n.*panic\(.*err[\)]*$ |
505 | 1.09% |
Plain Sentinels: | ^\s*(if|case|switch).*err == (?!nil).*{ |
313 | .67% |
EOF Checks: | ^\s*(if|case|switch).*err == .*EOF.*{ |
124 | .26% |
If errors.Is: | ^\s*(if|case|switch).*errors\.Is\( |
261 | .56% |
If errors.As: | ^\s*(if|case|switch).*errors\.As\( |
65 | .14% |
If errors.Is io.EOF: | ^\s*(if|case|switch).*errors\.Is\(.*EOF |
27 | .05% |
panic calls: | ^\s*panic\( |
2604 | 5.65% |
recover calls: | ^.*recover\(\) |
121 | .26% |
try keyword: | ^\s*\btry\b |
1 | 0% |
Error Handling Subject | RegEx | Count | % of 1st |
---|---|---|---|
All if (!=|==) nil variations: |
^\s*if.*err [!=]= nil |
29746 | 100.00% |
Auto Propagation: | ^\s*if.*err [!=]= nil {\n.*return.*err$ |
17389 | 58.45% |
Easy Propagation: | ^\s*if.*err [!=]= nil {\n.*return.*\.(Fatal|Errorf|New).*err[\)]*$ |
292 | .98% |
Panic Propagation: | ^\s*if.*err != nil {\n.*panic\(.*err[\)]*$ |
643 | 2.16% |
Plain Sentinels: | ^\s*(if|case|switch).*err == (?!nil).*{ |
79 | .26% |
EOF Checks: | ^\s*(if|case|switch).*err == .*EOF.*{ |
74 | .24% |
If errors.Is: | ^\s*(if|case|switch).*errors\.Is\( |
357 | 1.20% |
If errors.As: | ^\s*(if|case|switch).*errors\.As\( |
138 | .46% |
If errors.Is io.EOF: | ^\s*(if|case|switch).*errors\.Is\(.*EOF |
15 | .05% |
panic calls: | ^\s*panic\( |
4202 | 14.12% |
recover calls: | ^.*recover\(\) |
96 | .32% |
try keyword: | ^\s*\btry\b |
2 | 0% |
Clarifications:
- Auto Propagation means that in a repo, we could have used
Try Call
instead
ofif err != nil { return ..., err }
; we could easily refactor (simplify) them
with a script or a tool. - Easy Propagation means that in a repo, we could replace
if err != nil ...
withTry Call
if it clarifies error messages, or we could use deferred error
annotation 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
in
K8s. 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 are
surprisingly high, but the panic
score is explained by being a standard
library. However, the panic
usage is much higher than community police forces
claim.
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
or
errors.As
, you should wonder what all the fuss is about error
annotation.
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% of
cases, and most are io.EOF
checks. Weird? No, not actually.
It seems that the community has misunderstood the Go's error-handling
proverb:
Don't just check errors, handle them gracefully.
'... 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 similar
a keyword like in
Swift
and in Zig.
This section of the proposal has used the following specifications as a source
and copy&paste some parts of them directly:
Example of the language spec
We propose a new expression, Try Calls
to the language specification. The Try Calls
will be an extension to
Calls 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
Expression
-
wraps a function
f
to a new temporary (inline) functionf'
, and then -
invokes a function call for the temporary function
f'
that will invoke the
original functionf
and evaluates to the result of thef
call with the
final error result removed.More formally:
try f(a1, a2, … an) // where F(a1 T1, a2 T2, … an Tn) (RT1, RT2, …, RTn, error)
turns into
f'(f, a1, a2, … an) // where F'(f F, a1 T1, a2 T2, … an Tn) (RT1, RT2, …, RTn)
The f
must be a function or method call whose last result parameter is a value
of type error
. The try
with a function whose last result parameter is not a
value of type error
leads to a compile-time error.
The f
evaluates in a function or method call, producing n+1 result values of
types T1
, T2
, ... Tn
, and error
for the last value. If the function f
evaluates to a single value (n is 0), that value must be of type error
and
try f()
returns no result and can only appear as an expression
statement.
The try call
can be used in two different types of code context depending on
the 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 result
parameter where the last result is of typeerror
the following happens:Invoking
try
with a function callf()
as in (pseudo-code)func g() (T1, T2, …, error) { x1, x2, … xn = try f() // the line }
The line turns into the following (in-lined) code:
t1, … tn, te := f() // t1, … tn, te are local (invisible) temporaries if te != nil { _err = te // assign te to the error result parameter return // return from enclosing function } x1, … xn = t1, … tn // assignment only if there was no error
In other words, if the last value produced by the
f()
, of typeerror
, is
nil, thetry f()
simply returns the first n values, with the final nil
error stripped. If the last value produced by thef()
is not nil, the
enclosing 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 parameter
is not of typeerror
, the following happens:Invoking
try
with a function callf()
as in (pseudo-code)func g() { x1, x2, … xn = try f() // the line }
The line turns into the following (in-lined) code:
t1, … tn, te := f() // t1, … tn, te are local (invisible) temporaries if te != nil { panic(te) // transport error 'te' with panic } x1, … xn = t1, … tn // assignment only if there was no error
This version works similarly to the previous category. Only if the last value
produced by thef()
is not nil, the code panics with the current
error
value. When panicking, enclosing function's result parameters are
handled 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:
package sample
var (
defInstance = &Instance{
Counter: 0,
AAGUID: try uuid.Parse("12c....8-..af-4..d-b..f-f.....1a...1"),
Origin: try url.Parse(Origin),
}
Origin = "http://localhost"
)
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:
- K8s has a
try
named variable in tests. - Cockroach has similarly a couple of
try
named variables. - Also, Hugo has a couple of 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:
err = m.Up()
if errors.Is(err, migrate.ErrNoChange) {
glog.Info("no new migrations, skipping db modifications")
} else {
if err != nil {
...
}
After:
err = m.Up()
if errors.Is(err, migrate.ErrNoChange) {
glog.Info("no new migrations, skipping db modifications")
} else {
if err != nil {
...
}
- The error value is needed to make decisions; no code changes.
- If
if err != nil { return err }
would wanted to replace it can be done. See
the Orthogonality? chapter.
Example 2 - mixed error propagation
Before
func tarAddFS(w *tar.Writer, fsys fs.FS) error {
return fs.WalkDir(fsys, ".", func(name string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
info, err := d.Info()
if err != nil {
return err
}
h, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
h.Name = name
if err := w.WriteHeader(h); err != nil {
return err
}
f, err := fsys.Open(name)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(w, f)
return err
})
}
After:
func tarAddFS(w *tar.Writer, fsys fs.FS) error {
return fs.WalkDir(fsys, ".", func(name string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
info := try d.Info()
h := try tar.FileInfoHeader(info, "")
h.Name = name
try w.WriteHeader(h)
f := try fsys.Open(name)
defer f.Close()
try io.Copy(w, f)
return nil
})
}
- Did you notice how easy it was to skim important decision points?
- Did you get the idea what was going on faster?
- Did you, too, get the idea of a Go-style assertion package?
try assert.NoError(err) try assert.ThatNot(d.IsDir(), nil) ...
Example 3 - Classic copy file
Before:
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if _, err := io.Copy(w, r); err != nil {
w.Close()
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if err := w.Close(); err != nil {
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
return nil
}
- In practice, the above version stutters.
- It's full of DRY violations.
After:
func CopyFile(src, dst string) (err error) {
defer Annotate(&err, "copy")
r := try os.Open(src)
defer r.Close()
w := try os.Create(dst)
defer func() {
w.Close()
if err != nil {
os.Remove(dst) // only if a "try" fails below
}
}()
try io.Copy(w, r)
return nil
}
--- generic helper:
func Annotate(err *error, s string) {
if *err != nil {
*err = fmt.Errorf("%s: %w", s, *err)
}
- No stuttering
- Easier to skim
- FYI, it would work well without deferred helpers, e.g., part of the
cp
command in
Unix
Example 4 from the one possible future
Not part of the proposal
Optional - possibilities in the future.
(Existing of _err
would help implementation of recovererr()
)
// ↓ ↓ no named return parameters needed
func CopyFile(src, dst string) error {
errdefer Annotate("copy") // called only on error
r := try os.Open(src)
defer r.Close()
w := try os.Create(dst)
errdefer os.Remove(dst) // only on error
defer w.Close()
try io.Copy(w, r)
return nil
}
--- more possibilities to build helpers:
func Annotate(s string) error {
// this is called only if error happens => recovererr() returns error
return fmt.Errorf("%s: %w", s, recovererr()) // ←new builtin
}
func Handle(f func(err error) error ) { // could be used wrap os.Remove
err := f(recovererr())
if err != nil {
return fmt.Errorf("%w: %w", recovererr(), err) // new error
}
return recovererr() // error stays the same
}
- Glimpse of the potential future, which is only shown to bring more design
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.) - These only show us that if we want to continue language modifications, the most
prominent ones would be these:errdefer
is like the currentdefer
, but the deferred function is
called 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 instead
of returning possible panics,recovererr
returns possible error return
value of the function wheredefer
orerrdefer
was used. The same rules
apply asrecover
; it works only in deferred functions.recovererr
always returns an error if it's called inside an errdeferred function.
- as can be seen, they are aligned with the current
defer
-recover
rules and
are orthogonal in that way, too. - as also can be seen,
Try Calls
offer a path that is easy to extend and still
follow Go's principles
Examples when enclosing function does not return error
Example 5 usage in the main
function
In playground use, errors will trigger panics, and we will not stop them, which
is commonly OK in playgrounds.
func main() {
hex := try io.ReadAll(os.Stdin)
data := try parseHexdump(string(hex))
try os.Stdout.Write(data)
}
To stop panics and have proper error messages, add one line from some
helper package:
func main() {
defer err2.Catch()
hex := try io.ReadAll(os.Stdin)
data := try parseHexdump(string(hex))
try os.Stdout.Write(data)
}
Example 6 usage in tests
The panic functionality allows Try Calls
to work as-is with the current test
harness.
func TestCheckIntegrity(t *testing.T) {
// - OK versions
try alice.CheckIntegrity()
try bob.CheckIntegrity()
try carol.CheckIntegrity()
...
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. It
depends 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 the
function f
in try f()
stays in-lined similarly as without the try
then
there won't be any performance penalty.
Pure hypothetically, this compile-time build
Try Call
might open some new
optimization opportunities.
In cases where defer
is used for error handling, the error control flow
is a little slower, but the happy path inside the f
is the same. During the
prototype use, the main reason some functions were slower was the lack of
inlining because of the use of
defer
. That
might be one reason why a new errdefer
would be reasonable. It would bring
a 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 stack
traces 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 to
add declarative error handlers with defer
to your code.
Learnings from the usage of the prototype in several Go projects which have >100
KLOC:
- over four years of practice with the
err2
package has taught us that
nestedtry
is not desired or used. Searching the
code base wheretry.ToX
has been used only two (2) times in the nested way
wheretry.ToX
has been used 1144 times. - The prototype has taught us that
try
with a just variable (try err
) is rarely used or needed. It was used 15 times, and the
total was 1144. - The same package has taught that
if err
statements were still used
, especially with the channels. There were 80 error handling relatedif
statements. - Developers in projects have been pleased with the prototype.
- End-users have praised the error messages.
- Devops have thanked the runtime flags for error and stack traces.
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.
count := try try os.Open("data.txt").Read(data) // WRONG, compiler error
Our previous definition holds when try
is an operator and f()
is an operand.
The operand that try
operates must follow the conditions we defined in Try Call
chapter.
Because the try
'operator' has low precedence, we would need parenthesis to get
the previous code line to work:
count := try (try os.Open("data.txt")).Read(data) // Works (but is it ugly?)
In summary, the try
operator must build a new expression from the given operand (a
function call) before a Try Call
can be made, i.e., if you want to do
nested 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:
file := try os.Open("data.txt")
count := try file.Read(data)
Orthogonality?
As far as we know, this is orthogonal with the current language features.
- If you need to check the error value and transport it to the channel,
continue precisely as you would now without thetry
. - If your function usage is good to auto propagate, you perform a
Try Call
. - If your function usage happens inside a function that needs to annotate the
errors, you would still perform aTry Call
and annotate errors
in 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, the
operand is a function f()
, and the result is a function call. You cannot write
try err
. It will only compile with help. However, help is easy to arrange:
func noop(err error) error { return err } // OSS pkgs have this too.
func check(err error) error { return err } // ... Or readability?
...
try noop(err)
try check(err)
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 function
returning 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.
file := try os.Open("data.txt")
↓ Go-intuitive mapping ↑
file, err := os.Open("data.txt")
if err != nil {
return err
}
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. The
try
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.
file := try os.Open("data.txt")
count := try file.Read(data)
Vs.
file := try(os.Open("data.txt"))
count := try(file.Read(data))
The difference is slight but meaningful for some of us. For instance, the try
as a separate keyword aligns well with defer
and go
even
though these are statements. How convenient that they both start an implicit and
separated control flow.
defer f.Close()
try f.Close()
go f.Close()
Last but not least, we have learned that, e.g., dyslexic persons prefer wider
stance of the text.
Q: Why not guard
or must
?
A: We think both guard
and must
have different general semantics. For
example, Swift's guard
deals with boolean expressions and commonly needs
an else
-statement. And must
is more of a Go idiom for naming panic helpers. (These
are 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.
_, err := os.Stdin.Read(data)
if errors.Is(err, io.EOF) {
...
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 the
line, including the try
operator, you can select which control flow branch it
breaks. 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 in
Go, we could start to use try
as a source for automatic error traces, which
would be a great help in debugging.