Content-Length: 353246 | pFad | https://github.com/golang/go/issues/48855

8D Proposal: `on...return` for error handling · Issue #48855 · golang/go · GitHub
Skip to content

Proposal: on...return for error handling #48855

Closed
@jtlapp

Description

@jtlapp

This is a response to the 2018 request for error handling proposals. My inspiration derives in part from the concluding statement in Liam Breck's response.

I have what I hope is a simple addition that may address some of the discomfort that people have with error handling. I'm new to Go as of this past week, and I absolutely love everything about this language (so far) but how the error handling obscures the primary code path. I like to be able to see the structure and logic of the primary code path at a glance in order to help me clearly understand and evaluate its behavior.

Just to set the right expectations, I do not believe this proposal offers any functional benefit over existing error handling. Its purpose is purely to visually de-emphasize error handling in order to make the the primary code path more cognitively available, while still abiding by the code formatter's existing rules. I am however suggesting a new formatting rule for a new syntactic sugar syntax.

I propose a new statement having one of the following three forms (we'd pick one):

  • on <boolean-expression> return <optional-value>
  • on <reference> return <optional-value>
  • on <error> return <optional-value>

The proposal only addresses error handling that results in exiting the function, because I think code can generally be structured to fit this paradigm. I progressively modify the proposal as I go so you can see my thinking and the various alternatives.

Let's look at how we can clear up the following code, which I took from the above RFP:

func CopyFile_Original(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
}

We start with an equivalence of the following two statements:

  • on <boolean-expression> return <optional-value>
  • if <boolean-expression> { return <optional-value> }

Here is how this looks, but mind you, I have more suggestions to come:

func CopyFile_OnBoolReturn(src, dst string) (err error) {
	var localErr = func() error {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	r, err := os.Open(src)
	on err != nil return localErr()
	defer r.Close()

	w, err := os.Create(dst)
	on err != nil return localErr()

	if _, err := io.Copy(w, r); err != nil {
		w.Close()
		os.Remove(dst)
		return localErr()
	}

	if err := w.Close(); err != nil {
		os.Remove(dst)
		return localErr()
	}

	return nil
}

I moved the fmt.Errorf() out to a closure and used it as the return value for the two on...return statements. We could easily have done this by using the equivalent if statement code, but the code formatter would expand the if statements and clutter up the primary code path. The code formatter needs to know when not to expand the statement, and the on...return statement tells it just that.

Moreover, we always want the code formatter to expand if statements across multiple lines, because when we see an if statement heading a line all by itself with no indented lines following, warning lights signal in our mind that there is something wrong and needing correcting. Having these signals in our mind while trying to read the primary code path defeats the purpose of trying to make the primary code path more cognitively available. Hence, we would need a different keyword (on) that does not expect subsequent indentation.

But the err != nil is also repetitive and distracting, so we might instead follow on with a reference such that the return statement only executes when that reference is not nil. Now we have this:

func CopyFile_OnNotNil(src, dst string) (err error) {
	var localErr = func() error {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	r, err := os.Open(src)
	on err return localErr()
	defer r.Close()

	w, err := os.Create(dst)
	on err return localErr()

	if _, err := io.Copy(w, r); err != nil {
		w.Close()
		os.Remove(dst)
		return localErr()
	}

	if err := w.Close(); err != nil {
		os.Remove(dst)
		return localErr()
	}

	return nil
}

However, we still have the multi-statement handlers distracting us from the primary code path. We can address that with another vanilla Go closure:

func CopyFile_OnNotNilAndHandler(src, dst string) (err error) {
	localError := func() error {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	r, err := os.Open(src)
	on err return localError()
	defer r.Close()

	w, err := os.Create(dst)
	on err return localError()

	handler := func() error {
		w.Close()
		os.Remove(dst)
		return localErr()
	}
	_, err := io.Copy(w, r)
	on err return handler()

	if err := w.Close()
	on err return handler()

	return nil
}

Now things are looking pretty clean, but we still have error handling statements mixed in with the primary code path, distracting us from the primary code path. One way of dealing with that is to parenthesize them, though I'm not sure how fond I am of this approach:

func CopyFile_OnNotNilAndParentheses(src, dst string) (err error) {
	localError := func() error {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	r, err := os.Open(src)
	(on err return localError())
	defer r.Close()

	w, err := os.Create(dst)
	(on err return localError())

	handler := func() error {
		w.Close()
		os.Remove(dst)
		return localErr()
	}
	_, err := io.Copy(w, r)
	(on err return handler())
	if err := w.Close()
	(on err return handler())

	return nil
}

Another way to deal with this is to allow on...return to extend an existing statement. Perhaps we would require that the on reference be assigned in the preceding portion of the statement. Now we have this:

func CopyFile_OnReturnExtension(src, dst string) (err error) {
	localError := func() error {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	r, err := os.Open(src) on err return localError()
	defer r.Close()

	w, err := os.Create(dst) on err return localError()

	handler := func() error {
		w.Close()
		os.Remove(dst)
		return localErr()
	}
	_, err := io.Copy(w, r) on err return handler()
	err := w.Close() on err return handler()

	return nil
}

This approach assumes that the initial portions of the statements are short, which will often not be the case. We can deal with this by having the formatter indent on...return on the next line:

func CopyFile_OnReturnContinuation(src, dst string) (err error) {
	localError := func() error {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	r, err := os.Open(src)
		on err return localError()
	defer r.Close()

	w, err := os.Create(dst)
		on err return localError()

	handler := func() error {
		w.Close()
		os.Remove(dst)
		return localErr()
	}
	_, err := io.Copy(w, r)
		on err return handler()
	err := w.Close()
		on err return handler()

	return nil
}

I think the primary code path is pretty clear in the prior two examples, and the error handling code is right where it belongs as well.

To prevent abuse of on...return, we might restrict its reference to just the error type, assuming the reference approach is preferred over the boolean approach.

FYI, I briefly considered allowing the following construct, where the on statement need not always return from the function, but decided it might invite abuse:

	if _, err := io.Copy(w, r); on err {
		w.Close()
		os.Remove(dst)
		return localErr()
	}

(Apologies in advance if I'm misunderstanding something about Go, as I'm still a newbie.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeLanguageChangeSuggested changes to the Go languageProposalerror-handlingLanguage & library change proposals that are about error handling.v2An incompatible library change

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions









      ApplySandwichStrip

      pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


      --- a PPN by Garber Painting Akron. With Image Size Reduction included!

      Fetched URL: https://github.com/golang/go/issues/48855

      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy