golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
122.99k stars 17.54k forks source link

proposal: Go 2: `try` keyword for `Try Calls` #68391

Closed lainio closed 2 months ago

lainio commented 2 months ago

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:

Has this idea, or one like it, been proposed before?

Several are somehow similar; these are the most important ones:

  1. handle/check spec
  2. Try-proposal spec
  3. guard & must -proposal

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:

  1. proposal: errors/errd: helper package for deferred error handlers
How does this proposal differ?
  1. We leave the current error value-based handling as it is now.
  2. We bring a new expression to help calls to error-returning functions and propagate errors if they happen.
  3. 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:

  1. Those who cannot now use their full potential to skim Go code.
  2. Those who want to be able to refactor Go code quickly. See these examples why the current error handling makes it difficult.
  3. Everyone who is using some Incremental Hacking Cycle, TDD, etc.
  4. Polyglot developers.
  5. Everyone who has made a copy and past mistakes with their error annotations in Go.
  6. 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, Reddit (bold text annotations made by proposal author)

  1. 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.
  2. 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.
  3. 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.
  4. 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:

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.

We have manually checked the figures of the following two repos are correct:

kubernetes 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%
cockroach 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:

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:

  1. handle/check spec
  2. Try-proposal spec
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
  1. wraps a function f to a new temporary (inline) function f', and then
  2. invokes a function call for the temporary function f' that will invoke the original function f and evaluates to the result of the f 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:

  1. When the try call is used inside a function with at least one result parameter where the last result is of type error the following happens:

    Invoking try with a function call f() 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 type error, is nil, the try f() simply returns the first n values, with the final nil error stripped. If the last value produced by the f() 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).

  2. When the try call is used inside a function whose last result parameter is not of type error, the following happens:

    Invoking try with a function call f() 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 the f() 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:

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 {
     ...
}
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
    })
}
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
}

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)
     }
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
}
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:

  1. over four years of practice with the err2 package has taught us that nested try is not desired or used. Searching the code base where try.ToX has been used only two (2) times in the nested way where try.ToX has been used 1144 times.
  2. 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.
  3. The same package has taught that if err statements were still used , especially with the channels. There were 80 error handling related if statements.
  4. Developers in projects have been pleased with the prototype.
  5. End-users have praised the error messages.
  6. 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.

  1. If you need to check the error value and transport it to the channel, continue precisely as you would now without the try.
  2. If your function usage is good to auto propagate, you perform a Try Call.
  3. If your function usage happens inside a function that needs to annotate the errors, you would still perform a Try 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.

gabyhelp commented 2 months ago

Related Issues and Documentation

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

seankhliao commented 2 months ago

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.

ianlancetaylor commented 2 months ago

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:

The most crucial difference is that even the try keyword can execute an implicit control flow branch

Yes: that is the one of the main reasons that #32437 was declined.

lainio commented 2 months ago

@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 check wasn't the problem but the handle. Of course, as so many others have mentioned, we community members must gather our information from scratch. It's hard to know what's official and what's not.

Unfortunately, your replies didn't address our proposal as a whole. The chapter that spokes about implicit control flows states, among other things:

The most crucial difference is that even the try keyword can execute an implicit control flow branch, i.e., error propagation, it’s no different than the language’s current implicit control-flows. We use a keyword with well-known and straightforward semantics, allowing us to offer clear (and orthogonal) error propagation control flow.

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:

We leave the current error value-based handling as it is now.

[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 try() macro proposal:

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.

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 try() macro's proposal specification:

Yet, the context-sensitivity of try was considered fraught: For instance, the behavior of a function containing try calls could change silently (from possibly panicking to not panicking, and vice versa) if an error result was added or removed from the signature. This seemed too dangerous a property.

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.

ianlancetaylor commented 2 months ago

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.

lainio commented 2 months ago

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.

ianlancetaylor commented 2 months ago

Yes: if there is relevant new information, then we will reconsider an earlier decision.