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: orelse for streamlined error handling #61750

Closed
DrGo opened this issue Aug 4, 2023 · 24 comments
Closed

proposal: Go 2: orelse for streamlined error handling #61750

DrGo opened this issue Aug 4, 2023 · 24 comments
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal Proposal-FinalCommentPeriod v2 An incompatible library change
Milestone

Comments

@DrGo
Copy link
Contributor

DrGo commented Aug 4, 2023

as per @ianlancetaylor: "Please let's not have a discussion about error handling in general on this issue. Please only discuss this specific proposal. Thanks."

This proposal is not about avoiding typing few keystrokes or shirking responsibilities for error handling or about making error handling in Go like any other language. It simply aims to use the compiler to reduce the mental workload of reading/reviewing error-heavy code. Error handling with this approach will remain as explicit and as boring as ever!

Author background

  • Medical doctor/Professor who has been using Go since 2013 in several projects, and has experience with several other languages (c, Pascal, Javascript).

Proposal

I propose a new keyword orelse that is:

  • legal only, but not mandatory, following an assignment to a variable that satisfies the error interface, and
  • triggers the execution of an error-handling code block when the error variable is not nil. Other than being automatically triggered, there is nothing new or special about the triggered block.

Example

The example code in Russ Cox's paper[1] will look like this:

func CopyFile(src, dst string) error {
	//one line orelse 
	r, err := os.Open(src) orelse return fmt.Errorf("copy %s %s: %v", src, dst, err)
	defer r.Close()
	
	// orelse does not open a new scope
	w, err := os.Create(dst) orelse return fmt.Errorf("copy %s %s: %v", src, dst, err)
	
	// multi-line orelse block
	err := io.Copy(w, r) orelse {
		w.Close()
		os.Remove(dst)
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	err := w.Close() orelse  {
		os.Remove(dst)
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}
}

Rationale

The verbosity of Go's standard error handling approach is now a top concern for Go developers responding to the last annual survey. A more ergonomic approach should encourage adopting best practices of error handling and make code easier to read and maintain.

Compared to the current approach, I believe the proposed approach is:

  • significantly less verbose, e.g., the ratio of error-handling to program logic lines in the above program is 5:5 compared to 13:5 in the original sample,
  • as explicit; both the error-handling code and the potential for change in program flow are at least as clear,
  • no less flexible since it permits ignoring the error, returning it as is or wrapping it.

Because orelse is not used for any other purpose, it would be easy for reviewers and linters to spot lack of error handling. And because it is semantically similar to an else block, it should be easy to learn and understand.

Additional advantages include:

  • it builds on recent improvements in the standard errors package such as wrapping and unwrapping errors, e.g.,
_, err := io.ReadAll(r) orelse return errors.Wrap(err, "read failed")
  • it works well with named/bare returns, e.g.,
func returnObjOrErr() (obj Obj, err error) {
  obj, err := createObj() orelse return  //returns nil and err
}	
  • orelse does not open a new scope when it is not desired making it more ergonomic and reducing the risk for variable shadowing as in this example of a widely used idiom:
	if w, err:= os.Create(dst); err!= nil {} // new scope is opened preventing 
            // subsequent use of w and possibly shadowing a func-level err variable. 
  • it is still "Go-like"; each error is still handled individually although it should be easier to call a closure or a method to further deduplicate error handling. In this sense, it is similar to the try/handle approach without requiring an extra new keyword or inserting try in the middle of expressions.
func CopyFile(src, dst string) error {
    copyErr:= func(err error) {
		// do other stuff: log the error, set flags,
              //  format a nice error message etc
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }   
    r, err := os.Open(src) orelse return copyErr(err) 
  
    w, err := os.Create(dst);  orelse return copyErr(err)
    
   ... etc
}
  • it is backward compatible and does not prohibit those who prefer to use the current approach from continuing to use it.

  • it can handle the very rare situation when a function returns more than one error. The oresle block is invoked if any error is not nil.

Costs and risks

  • one extra keyword, although a familiar one and most previous proposals introduce at least one keyword or punctuation.
  • some proponents of the current approach may fear that it might encourage not handling errors. Whereas it is hard to be certain, I do suspect that having an ergonomic approach and a keyword dedicated to error handling will encourage proper error handling and make it easy to include correct examples of it in tutorials and code samples instead of skipping it as is often the case currently.
  • I do not foresee a significant impact on compile or runtime performance.

[1] https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md

@DrGo DrGo added LanguageChange Suggested changes to the Go language Proposal v2 An incompatible library change labels Aug 4, 2023
@gopherbot gopherbot added this to the Proposal milestone Aug 4, 2023
@gophun
Copy link

gophun commented Aug 4, 2023

I'm not arguing for or against this proposal, but it could simply use 'else' instead of introducing a new keyword. It should be unambiguous to parse in that position.

@seankhliao seankhliao added the error-handling Language & library change proposals that are about error handling. label Aug 4, 2023
@seankhliao
Copy link
Member

Besides the slightly different syntax, what makes this sufficiently distinct from previous proposals like #41908 #56895

@DrGo
Copy link
Contributor Author

DrGo commented Aug 4, 2023

Besides the slightly different syntax, what makes this sufficiently distinct from previous proposals like #41908 #56895

Thanks. I did not see the latter proposal despite searching the wiki entry on error handling..However, in my proposal I am focused on addressing the cultural issues that blocked progress on solving this problem. Also orelse is meant to be different from `else' to avoid giving the impression that it can be used anywhere where a returned value is not nil. This is specifically an error-handling mechanism.

@ianlancetaylor
Copy link
Contributor

Also looks similar to #32848 and #32946.

@DrGo
Copy link
Contributor Author

DrGo commented Aug 4, 2023

The solution space is very restricted... so it is not surprising that there will be similarities to previous proposals. The important thing here in my view is that we are having a discussion about an issue that is important to many Go users. Although I suspect that a vocal minority has already decided that the more verbose error handling the better, and that Go will die a slow painful death if another option was to be introduced!

@gophun
Copy link

gophun commented Aug 4, 2023

Although I suspect that a vocal minority has already decided that the more verbose error handling the better

It could also be that those who are unsatisfied with the current error handling are a vocal minority.

@ianlancetaylor
Copy link
Contributor

I agree that it is worth discussing. The place to discuss it is golang-nuts, and there is already an active discussion there. GitHub issues turn out to be bad at handling generalized discussions, because they are not threaded and when there are lots of comments they start hiding some. So if you want to discuss error handling, please do, but not here. Keep the issue tracker for specific proposals. Thanks.

@DrGo
Copy link
Contributor Author

DrGo commented Aug 4, 2023

No.. we have evidence from several surveys that it is a significant number of people. The important thing here is that the changes proposed do not affect those who prefer to use the current approach.. it does not complicate the language (in fact it simplifies error handling and learning and teaching it) and it does not affect compiling or runtime performance so it is hard to see why the resistance.

@DrGo
Copy link
Contributor Author

DrGo commented Aug 4, 2023

I agree that it is worth discussing. The place to discuss it is golang-nuts, and there is already an active discussion there. GitHub issues turn out to be bad at handling generalized discussions, because they are not threaded and when there are lots of comments they start hiding some. So if you want to discuss error handling, please do, but not here. Keep the issue tracker for specific proposals. Thanks.

I started that discussion on this particular proposal and overall the debate was encouraging so I proceeded to put a formal proposal.. I do not mean for this to be just a discussion. I think this is an appropriate time to resurface such a proposal after all the more urgent issues, e.g., modules and generics, were addressed. To me the obstacle to addressing this issue has been mostly cultural. I am hoping that the many recent changes in Go may have blunted the sharp divide between the error-handling camps so that we can find a compromise solution that works for all. Thanks

@gophun
Copy link

gophun commented Aug 4, 2023

No.. we have evidence from several surveys that it is a significant number of people.

The surveys indicate a significant number, but do not confirm whether it constitutes a majority. What we know is that no single error handling proposal has garnered a majority of support so far.

it does not complicate the language (in fact it simplifies error handling and learning and teaching it)

Introducing more ways of doing the same adds complexity to a language.

@DrGo
Copy link
Contributor Author

DrGo commented Aug 4, 2023

I think what happens with this issue is that the many people who want change are not as motivated as the "error handling must be painful camp".

Introducing more ways of doing the same adds complexity to a language.

That is a generalization.. often true. but not in this case. Complexity is not just about the count of keywords or ways of doing things.. it is anything that unnecessarily add to the mental load of the reader. I provided above some concrete way of measuring it in this particular case: the ratio of the number error handling lines to program logic lines.

@gophun
Copy link

gophun commented Aug 4, 2023

in fact it simplifies error handling and learning and teaching it

You can say a lot about the current way of error handling (boring, repetitive, etc.), but not that it's difficult to learn or teach. In fact it's so simplistic that even the noobiest of programming noobs understands it immediately.

@DrGo
Copy link
Contributor Author

DrGo commented Aug 4, 2023

this proposal does not change that, it will stay as boring as ever... it just creates a concise way that makes it easier to teach it e.g., by including it in code samples in tutorials instead of assigning the error to _ and asking the readers to manage the errors properly which is often the case even in the good Go books.

@ianlancetaylor
Copy link
Contributor

Please let's not have a discussion about error handling in general on this issue. Please only discuss this specific proposal. Thanks.

@ianlancetaylor
Copy link
Contributor

The solution space is very restricted... so it is not surprising that there will be similarities to previous proposals.

OK, but those previous proposals were rejected. We don't want to repeatedly revisit past decisions. If we are to consider a proposal that is similar to previously rejected proposals, we want to see new information that explains why this proposal is different.

@atdiar
Copy link

atdiar commented Aug 4, 2023

Does orelse allow any statement? Or just returns?

Note that the condition for the orelse statement is implicit here (err!= nil)

I'm not too sure about that.

( the advantage of the current error handling is that it uses the most basic of all control-flow structure, not sure that's something that can be replaced easily)

@DrGo
Copy link
Contributor Author

DrGo commented Aug 4, 2023

Thanks Ian,
I agree with not having general discussions here and will refrain from that.

I outlined several benefits of this proposal that I do not believe were discussed in the previous proposals including scoping of variables and integration with recently introduced facilities in the language. But as I mentioned I do believe the environment is different. This issue as I am sure you know has never been about the technical details; more about strongly held views about change which I hope have shifted a bit given all the recent developments.

@ianlancetaylor
Copy link
Contributor

A new keyword is not impossible, but it is a big lift for any language change. That is because introducing a new keyword will break any existing programs that use that keyword as a variable name, as in orelse := 0. That doesn't mean that we can't do it. All language changes are a cost/benefit decision. A new keyword is a heavy cost, and it requires a correspondingly big benefit.

This proposal changes

    r, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

into

    r, err := os.Open(src) orelse return fmt.Errorf("copy %s %s: %v", src, dst, err)

Ignoring the text that is the same, this changes \nif err != nil {\n\n} to orelse. That is, it changes four lines, six tokens, and 19 characters to one line, one token, and 6 characters.

Part of the boilerplate of error handling is , err on the left of the :=. This proposal doesn't remove that bit of boilerplate, unlike the try proposal (#32437).

The proposal introduces a syntax that is unlike any other syntax in Go, with an optional block. It moves error handling over to the right, which some like but others do not (see #57645, notably #57645 (comment)).

It's not entirely clear to me what is permitted to follow orelse other than return and {.

@DrGo
Copy link
Contributor Author

DrGo commented Aug 4, 2023

Does orelse allow any statement? Or just returns?

Good question. I think it should be only return or {...} block

@DrGo
Copy link
Contributor Author

DrGo commented Aug 4, 2023

Does orelse allow any statement? Or just returns?

Note that the condition for the orelse statement is implicit here (err!= nil)

I'm not too sure about that.

( the advantage of the current error handling is that it uses the most basic of all control-flow structure, not sure that's something that can be replaced easily)

I guess what I am saying is that the orelse (instead eg of just else) dedicated keyword makes the potential change in program flow due to error explicit.

@DrGo
Copy link
Contributor Author

DrGo commented Aug 4, 2023

Ignoring the text that is the same, this changes \nif err != nil {\n\n} to orelse. That is, it changes four lines, six tokens, and 19 characters to one line, one token, and 6 characters.

I suggested a metric to measure the reduction in boilerplate (# err handling lines: # program logic lines) which I think is a fair way to measure the mental burden of reading and understanding error-heavy code like the CopyFile routine. I prefer it to counting tokens in a single error handling line because the latter does not account for mental obstacles like the need to scroll back and forth to see the entirety of a routine (which is especially true for a learner or a code reviewer).

Part of the boilerplate of error handling is , err on the left of the :=. This proposal doesn't remove that bit of boilerplate, unlike the try proposal (#32437).

correct...I intentionally left that in because it makes it easier to read subsequent code (it is clear what the err var refers to in the orelse block)..

The proposal introduces a syntax that is unlike any other syntax in Go, with an optional block.

The block can be made mandatory like with other conditionals.

It's not entirely clear to me what is permitted to follow orelse other than return and {.

was thinking just a {} or a return as a special case (but see above)

@m3ok
Copy link

m3ok commented Aug 29, 2023

After read the proposal, I'd like to introduce a derivative idea for consideration. The aim is to provide a more concise way this, while remaining explicit and consistent with Go's established idioms.

f, err := os.Open("file.txt"); else { return err }
data, err := ioutil.ReadAll(f); else { return err }
// continue processing data

I don't want to open similar proposal for this one. Just an idea.

@ianlancetaylor
Copy link
Contributor

Based on the discussion above, the similarity to previously declined proposals, and the emoji voting, this is a likely decline. Leaving open for three weeks for final comments.

@ianlancetaylor
Copy link
Contributor

No further comments.

@ianlancetaylor ianlancetaylor closed this as not planned Won't fix, can't repro, duplicate, stale Oct 4, 2023
@golang golang locked and limited conversation to collaborators Oct 3, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal Proposal-FinalCommentPeriod v2 An incompatible library change
Projects
None yet
Development

No branches or pull requests

7 participants