Skip to content
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

proposal: Go 2: use keywords throw, catch, and guard to handle errors #40583

Closed
ysmood opened this issue Aug 5, 2020 · 24 comments
Closed

proposal: Go 2: use keywords throw, catch, and guard to handle errors #40583

ysmood opened this issue Aug 5, 2020 · 24 comments

Comments

@ysmood
Copy link

@ysmood ysmood commented Aug 5, 2020

The proposal borrowed some ideas from The check/handle proposal.
One of the issues of "check/handle proposal" is that it doesn't support function chaining well: https://gist.github.com/DeedleFake/5e8e9e39203dff4839793981f79123aa.

To make a long story short, let's see some code examples first.

Without the proposal:

type data struct {}

func (d data) bar() (string, error) {
    return "", errors.New("err")
}

func foo() (data, error) {
    return data{}, errors.New("err")
}

func do () (string, error) {
    d, err := foo()
    if err != nil {
        return "", err
    }

    s, err := d.bar()
    if err != nil {
        return "", err
    }

    return s, nil
}

With the proposal:

type data struct {}

func (d data) bar() string {
    // The throw is like the return, you have to state the origin return values with an extra error.
    // The last value of the list must fulfil error interface, or compile error.
    throw "", errors.New("err")
}

func foo() (d data) {
    // Same as the return, in this case we can omit the return values.
    throw errors.New("err")

    // Throw is like return, this line is unreachable.
    return
}

func do () (string, error) {
    // The catch is similar to the handle in the check/handle proposal.
    catch err { // the type of err is error
        return "", err // you must return corresponding values just like a normal function
    }

    // The error will propagate like the panic until it's caught by a `catch` clause.
    s := foo().bar()
    return s, nil
}

Use keyword guard to convert the throw version to normal error value style, so that we can precisely handle each error separately:

func do () (string, error) {
    d, err := guard foo()
    if err != nil {
        return "", err
    }

    s, err := guard d.bar()
    if err != nil {
        return "", err
    }

    return s, nil
}

Currently, I created a demo lib to simulate the taste: https://github.com/ysmood/tcg/blob/master/example_test.go
We shouldn't just use the panic to do it. For the implementation of throw, it's similar to setjmp and longjmp. I feel the performance won't be affected.

To avoid forgetting catch we can statically do something similar to https://github.com/kisielk/errcheck by analyzing throw keyword.

Sure the keywords are not decided, we can use other words if they are better.


Would you consider yourself a novice, intermediate, or experienced Go programmer?

Experienced

What other languages do you have experience with?

js, ruby, elixir, scala,c, c#, python, haskell, php, lua

Would this change make Go easier or harder to learn, and why?

Harder

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

Have researched the #40432, not able to find similar ones.

Who does this proposal help, and why?

Everyone, because error handling is used in most of projects.

What is the proposed change?

See the front description.

Is this change backward compatible?

No, it will add new keywords.

What is the cost of this proposal? (Every language change has a cost).

Users have to spend more time to master the error handling.

How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?

All of them.

What is the compile time cost?

Minor cost.

What is the run time cost?

Minor cost.

Can you describe a possible implementation?

Only if we agree with this idea.

Do you have a prototype? (This is not required.)

No

How would the language spec change?

See the front description.

Orthogonality: how does this change interact or overlap with existing features?

Might overlap the panic.

Is the goal of this change a performance improvement?

No.

Does this affect error handling?

Yes.

If so, how does this differ from previous error handling proposals?

See the front description.

Is this about generics?

No.

@gopherbot gopherbot added this to the Proposal milestone Aug 5, 2020
@gopherbot gopherbot added the Proposal label Aug 5, 2020
@davecheney
Copy link
Contributor

@davecheney davecheney commented Aug 5, 2020

In the strongest possible terms, no.

@martisch
Copy link
Contributor

@martisch martisch commented Aug 5, 2020

See #40432.

Especially: https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md
For why implicit checking/implicit results is not prefered for error handling.

@deltamualpha
Copy link

@deltamualpha deltamualpha commented Aug 5, 2020

Just to play around with this a little, how do defers work in this framework? Does a throw still run queued defer statements? Would they run after the thrown exception was caught and handled?

How can callers know that a function can throw (the "implicit checking/results" problem)? Pushing that into tooling doesn't seem to really solve the problem, because it requires people to set up and use the tool globally and consistently.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Aug 5, 2020

For language change proposals, please fill out the template at https://go.googlesource.com/proposal/+/refs/heads/master/go2-language-changes.md .

When you are done, please reply to the issue with @gopherbot please remove label WaitingForInfo.

Thanks!

@ysmood
Copy link
Author

@ysmood ysmood commented Aug 6, 2020

@gopherbot please remove label WaitingForInfo.

@ysmood
Copy link
Author

@ysmood ysmood commented Aug 6, 2020

@deltamualpha

how do defers work in this framework? Does a throw still run queued defer statements? Would they run after the thrown exception was caught and handled?

It doesn't affect how defer works. It's standalone, similar to the "check/handle proposal".

How can callers know that a function can throw (the "implicit checking/results" problem)? Pushing that into tooling doesn't seem to really solve the problem, because it requires people to set up and use the tool globally and consistently.

For example

fmt.Fprintln(os.Stdout, "")

If you don't use tools like errcheck, people will forget to handle the error here.

Why can't we use a similar tool to indicate if a function will throw? In IDE it might look like this:

throw

Just for the demo, we could use a better sign or color other than !.

It's very easy to recursively analyze the code to know if a throw is not caught by any catch.

@martisch
Copy link
Contributor

@martisch martisch commented Aug 6, 2020

It doesn't affect how defer works.

Throws semantics need to be explicitly defined as its introduces new control flow.
What happens when a throw is executed in a defer? Do the other defers still run?

It's very easy to recursively analyze the code to know if a throw is not caught by any catch.

This is not possible statically when reflect Call, function variables and plugins are involved without explicit anotation.

Errors are different in that the function signature shows explicitly whether an error is returned or not.

@ysmood
Copy link
Author

@ysmood ysmood commented Aug 6, 2020

This is not possible statically when reflect Call, function variables and plugins are involved without explicit anotation.

I think it's the same as Go1's error. If you return an error as an interface, it will be hard to tell when a function will return an error or not.

What happens when a throw is executed in a defer? Do the other defers still run?

Good point, I will take some time to add some examples for them.

@martisch
Copy link
Contributor

@martisch martisch commented Aug 6, 2020

I think it's the same as Go1's error. If you return an error as an interface, it will be hard to tell when a function will return an error or not.

True but there was no claim here that its possible to know where all error values are originating from either. Errors dominately are returned as error types directly which is possible to analyse statically and not as interface{}. This analysis also doesnt need whole program inter package analysis as knowing function signatures is enough. As there would be no explicit annotation for throws the analysis is quite a bit harder and has more commonly used language cases like function variables and reflect Calls where it will expensive or not knowable if a throw can happen.

@ysmood
Copy link
Author

@ysmood ysmood commented Aug 6, 2020

At least we should avoid talking about "plugin" which is even unusable on Windows: #19282 (this 2017 issue is still open)

For reflect, even you try to use reflect to hide throw, you will end up calling something like relfect.Throw which can still be statically analyzed.

@martisch
Copy link
Contributor

@martisch martisch commented Aug 6, 2020

reflect can call normal go methods by name just fine. There is not constraint in the proposal or language that enforces functions called with reflect to need to use reflect.Throw.

Its similar how fmt can not statically know if a Stringer implementing type that is passed in has the potential to panic or not. It needs to assume any function can panic.

fmt.Printf("%s", foo) what types of exceptions would foo's potential String method be able to throw? foo might be any type due to being stored in an interface{} and handed around in the code.

At any rate. These question all reflect that implicit error checking/flow has issues and I dont think they can be satisfactory solved trying to mitigate them with expensive static analysis.

@ysmood
Copy link
Author

@ysmood ysmood commented Aug 6, 2020

fmt.Printf("%s", foo) what types of exceptions would foo's potential String method be able to throw? foo might be any type due to being stored in an interface{} and handed around in the code.

The analyzer can tell you are passing function foo that can throw to function fmt.Printf that doesn't have catch to handle the throw.

@martisch
Copy link
Contributor

@martisch martisch commented Aug 6, 2020

The analyzer can tell you are passing function foo that can throw to function fmt.Printf that doesn't have catch to handle the throw.

The analyzer can not know that in all cases if e.g. foo being an interface{} value (containing a type implementing Formatter or Stringer or something else) that came from somewhere populated at run time. The analyzer can be defensive and if can not be proven otherwise always assume there can be a throw but then there will be lots of places annotated with can throw which can lead to alot of boilerplate catching.

I dont think the analyzer will be fast (requires whole program analysis) nor easy and it requires to use an IDE to better understand where errors/exceptions can come from. Happy to be proven otherwise.

@ysmood
Copy link
Author

@ysmood ysmood commented Aug 6, 2020

foo being an interface{} value

If we use reflect to create a DSL, we literally can do whatever we want without any constrains. We can even use reflect to dynamically create functions with dynamic signatures. Even with Go1's error, it will be hard to understand the code. This proposal is not meant to improve how we use reflect.

The analyzer can be defensive and if can not be proven otherwise always assume there can be a throw but then there will be lots of places annotated with can throw which can lead to alot of boilerplate catching.

Such as python or javascript, they don't use catch everywhere. As long as you catch at their parent node, the analyzer shouldn't complain.

I dont think the analyzer will be fast (requires whole program analysis) nor easy and it requires to use an IDE to better understand where errors/exceptions can come from. Happy to be proven otherwise.

I think the complexity is O(n). As long as it's not O(n^2) I think we shouldn't define it's slow. We have a good language server which can cache the result. One node's change won't affect the whole tree, it will only affect functions depend on it.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Aug 6, 2020

Have you seen https://golang.org/doc/faq#exceptions ?

That said:

I don't understand the difference between throw and panic.

I don't understand the difference between catch and a deferred function that calls recover.

I don't understand the difference between guard and

func Guard(f func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
           err = r.(error)
        }
    }()
    f()
}
@ysmood
Copy link
Author

@ysmood ysmood commented Aug 6, 2020

@ianlancetaylor

Have you seen https://golang.org/doc/faq#exceptions ?

Yes, I use Go for many years now, I think I know most of the debates and how hard it will be to challenge such a topic.

I don't understand the difference between throw and panic

There are a lot. For example, panic will crash the application, throw won't. It's the same as when you forget to handle the err returned from a function, nothing will happen. We can discuss how to handle uncaught errors. Such as nodejs' uncaught exception.

I don't understand the difference between catch and a deferred function that calls recover.

My example above already showed, in catch you have to return all values of the return signature. defer doesn't.

I don't understand the difference between guard and

guard can handle and return generic type, just like the append(T, ...T) T can accept and return type in a generic fashion.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Aug 6, 2020

Thanks. I think you need to write down the precise semantics of these new constructs, because I do not understand them. All I've seen so far is examples and implementation. My apologies if I've missed a description.

@ysmood
Copy link
Author

@ysmood ysmood commented Aug 7, 2020

@ianlancetaylor Thank you for your feedback. After collecting more feedbacks I will try to add precise semantics, I may also modify the specs while reviewing them. I searched things like "golang where to share language draft design" with no luck. So I shared it here, hope to find people who have the willingness to work on this idea together.

@davecheney
Copy link
Contributor

@davecheney davecheney commented Aug 7, 2020

@ysmood this may be useful for your proposal. https://github.com/golang/proposal#proposing-changes-to-go

@deanveloper
Copy link

@deanveloper deanveloper commented Aug 7, 2020

you didn't fix the chaining issue. the goal seemed to be making chaining easier (so that one could do foo().bar() with proper error handling), but you still didn't allow chaining with your solution. i'm confused about what problem this solves.

@ysmood
Copy link
Author

@ysmood ysmood commented Aug 8, 2020

but you still didn't allow chaining with your solution

Sorry, what you do mean? Can you give more details about your concern?

@deanveloper
Copy link

@deanveloper deanveloper commented Aug 8, 2020

i had misread part of the proposal, my bad. i was confused for a reason, haha

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Aug 18, 2020

Based on the discussion above and the emoji voting, this is a likely decline. Leaving open for four weeks for final comments.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Sep 15, 2020

No further discussion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
8 participants
You can’t perform that action at this time.