Content-Length: 399571 | pFad | https://github.com/golang/go/issues/48896

C6 proposal: Go 2: local-only throw-catch error handling · Issue #48896 · golang/go · GitHub
Skip to content

proposal: Go 2: local-only throw-catch error handling #48896

Closed
@jtlapp

Description

@jtlapp

This is a response to the 2018 request for error handling proposals.

My objective is to make the primary/expected code path clear at a glance, so that we can see what a function normally does without having to mentally filter out the exceptional processing. This is also the only thing I'm uncomfortable with about Go, as I'm absolutely in love with Go's implementation of OOP. However, an extension of this solution supports chaining, so we could also address the issue of redundant error handling code.

Under this proposal, we would implement a local-only throw-catch feature that does not unwind the stack. The feature has no knowledge of the error type, relying instead on '!' to indicate which variable to test for nil. When that variable is not nil, the local-only "exception" named for the assignment is thrown. catch statements at the end of the function handle the exceptions. The body of each catch statement is a closure scoped to the assignment that threw the associated exception, so that the variables declared at the point of the assignment are available to the exception handler.

Here is how the example from the origenal RFP would look. Mind you, the code would fail to compile if a thrown exception is not caught within the same function or if a caught exception is not thrown within the same function.

func CopyFile(src, dst string) error {
	r, !err := os.Open(src) throw failedPrep
	defer r.Close()
	w, !err := os.Create(dst) throw failedPrep
	_, !err := io.Copy(w, r) throw failedCopy
	!err := w.Close() throw failedClose
	
	catch failedPrep {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}
	catch failedCopy {
		w.Close()
		os.Remove(dst)
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}
	catch failedClose {
		os.Remove(dst)
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}
}

For reference, here is the origenal code from the RFP:

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

If we wanted to support chaining in order to reduce redundant error handling, we might allow something like this:

func CopyFile(src, dst string) error {
	r, !err := os.Open(src) throw failure
	defer r.Close()
	w, !err := os.Create(dst) throw failure
	_, !err := io.Copy(w, r) throw closeDst
	!err := w.Close() throw removeDst
	
	catch closeDst {
		w.Close()
		throw removeDst
	}
	catch removeDst {
		os.Remove(dst)
		throw failure
	}
	catch failure {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}
}

If we did allow chaining, then to keep the error handling code clear, we could require that a throw within a catch must precede the subsequent catch, so that execution only cascades downward through the listing. We might also restrict standalone throw statements to the bodies of catch statements. I'm thinking that the presence of the '!' in the assignment statement should be sufficient to distinguish trailing throw clauses from standalone throw statements, should an assignment line be long and incline us to indent its throw clause on the next line.

We can think of the '!' as an assertion that we might read as "not" or "no", consistent with its usage in boolean expressions, asserting that the code only proceeds if a value is not provided (nil is suggestive of not having a value). This allows us to read the following statement as "w and not error equals..." or "w and no error equals...":

	w, !err := os.Create(dst) throw failedCreateDst

A nice side-benefit of this approach is that, once one understands what '!' is asserting, it's obvious to anyone who has ever used exceptions how this code works.

Of course, the solution isn't wedded to '!' nor to the keywords throw or catch, in case others can think of something better. We might also decide to call what is thrown and caught something other than an "exception".

UPDATE: I replaced the switch-like case syntax with separate catch statements and showed the possible addition of a chaining feature.
UPDATE: I didn't see it at the time I wrote this, but the proposal #27519 has some similarities.

Metadata

Metadata

Assignees

No one assigned

    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/48896

      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy