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: spec: reduce error handling boilerplate using ? #71203

Closed
ianlancetaylor opened this issue Jan 9, 2025 · 251 comments
Closed

proposal: spec: reduce error handling boilerplate using ? #71203

ianlancetaylor opened this issue Jan 9, 2025 · 251 comments
Labels
error-handling Language & library change proposals that are about error handling. LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Milestone

Comments

@ianlancetaylor
Copy link
Member

ianlancetaylor commented Jan 9, 2025

Proposal Details

Background

As discussed in the introduction to an earlier, declined, proposal, Go programs have a lot of error checking code. In surveys error handling is listed as the biggest specific challenge people face using Go today.

There have been many proposals to address this, summarized in a meta-proposal.

This is yet another such proposal. This proposal has some similarities to onerr return, add "or err: statement" after function calls for error handling, and use ? simplify handling of multiple-return-values. It is in some ways a reworking of simplify error handling with || err suffix. There are probably a number of other proposals that fed into this one even if I can't remember them now.

The goal of this proposal is to introduce a new syntax that reduces the amount of code required to check errors in the normal case, without obscuring flow of control.

New syntax

This section is an informal description of the proposal, with examples. A more precise description appears below.

I propose permitting statements of the form

	r, err := SomeFunction()
	if err != nil {
		return fmt.Errorf("something failed: %v", err)
	}

to be written as

	r := SomeFunction() ? {
		return fmt.Errorf("something failed: %v", err)
	}

The ? absorbs the error result of the function. It introduces a new block, which is executed if the error result is not nil. Within the new block, the identifier err refers to the absorbed error result.

Similarly, statements of the form

	if err := SomeFunction2(); err != nil {
		return fmt.Errorf("something else failed: %v", err)
	}

may be written as

	SomeFunction2() ? {
		return fmt.Errorf("something else failed: %v", err)
	}

Further, I propose that the block following the ? is optional. If the block is omitted, it acts as though there were a block that simply returns the error from the function. For example, code like

	if err := SomeFunction2(); err != nil {
		return err
	}

may in many cases be written as

	SomeFunction2() ?

Formal proposal

This section presents the formal proposal.

An assignment or expression statement may be followed by a question mark (?). The question mark is a new syntactic element, the first permitted use of ? in Go outside of string and character constants. The ? causes conditional execution similar to an if statement. A ? at the end of a line causes a semicolon to be automatically inserted after it.

A ? uses a value as described below, referred to here as the qvalue.

For a ? after an assignment statement, the qvalue is the last of the values produced by the right hand side of the assignment. The number of variables on the left hand side of the assignment must be one less than the number of values produced by the right hand side (the right hand side values may come from a function call as usual). It is not valid to use a ? if there is only one value on the right hand side of the assignment.

For a ? after an expression statement the qvalue is the last of the values of the expression. It is not valid to use a ? after an expression statement that has no values.

The qvalue must be of interface type and must implement the predeclared type error; that is, it must have the method Error() string. In most cases it will simply be of type error.

A ? is optionally followed by a block. The block may be omitted if the statement using ? appears in the body of a function, and the enclosing function has at least one result, and the qvalue is assignable to the last result (this means that the type of the last result must implement the predeclared type error, and will often simply be error).

Execution of the ? depends on the qvalue. If the qvalue is nil, execution proceeds as normal, skipping over the block if there is one.

If the ? is not followed by a block, and the qvalue is not nil, then the function returns immediately. The qvalue is assigned to the final result. If the other results (if any) are named, they retain their current values. If they are not named, they are set to the zero value of their type. The results are then returned. Deferred functions are executed as usual.

If the ? is followed by a block, and the qvalue is not nil, then the block is executed. Within the block a new variable err is implicitly declared, possibly shadowing other variables named err. The value and type of this err variable will be those of the qvalue.

That completes the proposal.

Examples

func Run() error {
	Start() ? // returns error from Start if not nil
	Wait() ?  // returns error from Wait if not nil
	return nil
}
func CopyFile(src, dst string) error {
	r := os.Open(src) ? {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}
	defer r.Close()

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

	io.Copy(w, r) ? {
		w.Close()
		os.Remove(dst)
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	w.Close() ? {
		os.Remove(dst)
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}
}
func MustOpen(n string) *os.File {
	f := os.Open(n) ? {
		panic(err)
	}
	return f
}
func TestFileData(t *testing.T) {
	f := os.Open("testfile") ? {
		t.Fatal(err)
	}
	...
}
func CreateIfNotExist(name string) error {
	f, err := os.OpenFile(name, os.O_EXCL|os.O_WRONLY, 0o666)
	if errors.Is(err, fs.ErrExist) {
		return nil
	}
	err ? // returns err if it is not nil
	// write to f ...
}

Discussion

This new syntax is partly inspired by Rust's question mark operator, though Rust permits ? to appear in the middle of an expression and does not support the optional block. Also, I am suggesting that gofmt will enforce a space before the ?, which doesn't seem to be how Rust is normally written.

Absorbing the error returned by a function, and optionally returning automatically if the error is not nil, is similar to the earlier try proposal. However, it differs in that

  • ? is an explicit syntactic element, not a call to a predeclared function, and
  • ? may only appear at the end of the statement, not in the middle of an expression.

Declaring the err variable

As discussed above, when a block follows the ? it implicitly declares a new err variable. There are no other cases in Go where we implicitly declare a new variable in a scope. Despite that fact, I believe this is the right compromise to maintain readability while reducing boilerplate.

A common suggestion among early readers of this proposal is to declare the variable explicitly, for example by writing

	r := SomeFunction() ? err {
		return fmt.Errorf("something failed: %v", err)
	}

In practice, though, the variable would essentially always be simply err. This would just become additional boilerplate. Since the main goal of this proposal is to reduce boilerplate, I believe that we should try our best to do just that, and introduce err in the scope rather than requiring people to declare it explicitly.

If the implicit declaration of err seems too problematic, another approach would be to introduce a new predeclared name. The name err would not be appropriate here, as that would be too often shadowed in existing code. However, a name like errval or erv would work. Within a ? optional block, this name would evaluate to the qvalue. Outside of a ? optional block, referring to the name would be a compilation error. This would have some similarities to the predeclared name iota, which is only valid within a const declaration.

A third approach would be for errval or erv to be a predeclared function that returns the qvalue.

Supporting other types

As discussed above the qvalue must be an interface type that implements error. It would be possible to support other interface types. However, the ? operator, and especially the implicitly declared err variable, is specifically for error handling. Supporting other types confuses that focus. Using ? with non-error types would also be confusing for the reader. Keeping a focus on just handling errors seems best.

It would also be possible to support non-interface types that implement error, such as the standard library type *os.SyscallError. However, returning a value of that type from a function that returns error would mean that the function always returns a non-nil error value, as discussed in the FAQ. Using different rules for ? would make an already-confusing case even more confusing.

Effects on standard library

I applied a simple rewriter to the standard library to introduce uses of ? where feasible. Here are some examples of new code:

archive/tar/common.go:

		h.Gname = iface.Gname() ?
		h.Uname = iface.Uname() ?

archive/tar/writer_test.go:

	// Test that we can get a long name back out of the archive.
	reader := NewReader(&buf)
	hdr = reader.Next() ? {
		t.Fatal(err)
	}
	if hdr.Name != longName {
		...

archive/zip/reader.go:

	var buf [directoryHeaderLen]byte
	io.ReadFull(r, buf[:]) ?

archive/zip/reader.go:

		p, err := findDirectory64End(r, directoryEndOffset)
		if err == nil && p >= 0 {
			directoryEndOffset = p
			err = readDirectory64End(r, p, d)
		}
		err ?

archive/zip/reader_test.go:

	b := hex.DecodeString(s) ? {
		panic(err)
	}

cmd/cgo/godefs.go:

func gofmt(n interface{}) string {
	gofmtBuf.Reset()
	printer.Fprint(&gofmtBuf, fset, n) ? {
		return "<" + err.Error() + ">"
	}
	return gofmtBuf.String()
}

cmd/cgo/out.go:

		fexp := creat(*exportHeader)
		fgcch := os.Open(*objDir + "_cgo_export.h") ? {
			fatalf("%s", err)
		}
		defer fgcch.Close()
		io.Copy(fexp, fgcch) ? {
			fatalf("%s", err)
		}
		fexp.Close() ? {
			fatalf("%s", err)
		}

os/exec/exec.go:

func (c *Cmd) Run() error {
	c.Start() ?
	return c.Wait()
}

The conversion tool found 544,294 statements in the standard library. It was able to convert 8820 of them to use ?. In all, 1.6% of all statements were changed. 1380 statements, or 0.25% of the total, were changed to use a ? with no optional block.

In other words, adopting this change across the ecosystem would touch an enormous number of lines of existing Go code. Of course, changing existing code could happen over time, or be skipped entirely, as current code would continue to work just fine.

Pros and cons

Pros

Advantage 1: Rewriting

	r, err := SomeFunction()
	if err != nil {
		return fmt.Errorf("something failed: %v", err)
	}

to

	r := SomeFunction() ? {
		return fmt.Errorf("something failed: %v", err)
	}

reduces the error handling boilerplate from 9 tokens to 5, 24 non-whitespace characters to 12, and 3 boilerplate lines to 2.

Rewriting

	r, err := SomeFunction()
	if err != nil {
		return err
	}

to

	r := SomeFunction() ?

reduces boilerplate from 9 tokens to 1, 24 non-whitespace characters to 1, and 3 boilerplate lines to 0.

Advantage 2: This change turns the main code flow into a straight line, with no intrusive if err != nil statements and no obscuring if v, err = F() { … } statements. All error handling either disappears or is indented into a block.

Advantage 3: That said, when a block is used the } remains on a line by itself, unindented, as a signal that something is happening. (I'm also listing this as a disadvantage, below.)

Advantage 4: Unlike the try proposal and some other error handling proposals, there is no hidden control flow. The control flow is called out by an explicit ? operator that can't be in the middle of an expression, though admittedly the operator is small and perhaps easy to miss at the end of the line. I hope the blank before it will make it more visible.

Advantage 5: To some extent this reduces a couple of common error handling patterns to just one, as there is no need to decide between

	var (
		v   string
		err error
	)
	if v, err = F(); err != nil {
		...
	}

and

	v, err := F()
	if err != nil {
		...
	}

Instead people can consistently write

	v := F() ? {
		...
	}

Cons

Disadvantage 1: This is unlike existing languages, which may make it harder for novices to understand. As noted above it is similar to the Rust ? operator, but still different. However, it may not be too bad: Todd Kulesza did a user study and discovered that people unfamiliar with the syntax were able to see that the code had to do with error handling.

Disadvantage 2: The shadowing of any existing err variable may be confusing. Here is an example from the standard library where the ? operator can not be easily used:

fmt/scan.go:

	for n = 1; !utf8.FullRune(r.buf[:n]); n++ {
		r.buf[n], err = r.readByte()
		if err != nil {
			if err == io.EOF {
				err = nil // must change outer err
				break
			}
			return
		}
	}
	// code that later returns err

In this example the assignment err = nil has to change the err variable that exists outside of the for loop. Using the ? operator would introduce a new err variable shadowing the outer one. (In this example using the ? operator would cause a compiler error, because the assignment err = nil would set a variable that is never used.)

Disadvantage 3: When using a block, the } remains on a line itself, taking up space as pure boilerplate. (I'm also listing this as an advantage, above.)

Disadvantage 4: No other block in Go is optional. The semicolon insertion rule, and the fact that a block is permitted where a statement is permitted, means that inserting or removing a newline can convert one valid Go program into another. As far as I know, that is not true today.

For example, these two functions would both be valid and have different meanings, although the only difference is whitespace.

func F1() error {
	err := G1()
	log.Print(err)
	G2() ?
	{
		log.Print(err)
	}
	return nil
}

func F2() error {
	err := G1()
	log.Print(err)
	G2() ? {
		log.Print(err)
	}
	return nil
}

Disadvantage 5: For an expression statement that just calls a function that returns an error, it's easy to accidentally forget the ? and write F() rather than F() ?. Of course it's already easy to forget to check the error result, but once people become accustomed to this proposal it may be easy to overlook the missing ? when reading code.

Disadvantage 6: This proposal has no support for chaining function calls, as in F().G().H(), where F and G also have an error result.

Disadvantage 7: This proposal makes it easier to simply return an error than to annotate the error, by using a plain ? with no block. This may encourage programmers to skip error annotations even when they are desirable.

Disadvantage 8: We really only get one chance to change error handling syntax in Go. We aren't going to make a second change that touches 1.5% of the lines of existing Go code. Is this proposal the best that we can do?

Disadvantage 9: We don't actually have to make any changes to error handling. Although it is a common complaint about Go, it's clear that Go is usable today. Perhaps no change is better than this change. Perhaps no change is better than any change.

Transition

If we adopt this proposal, we should provide tools that can be used to automatically rewrite existing Go code into the new syntax. Not everyone will want to run such a tool, but many people will. Using such a tool will encourage Go code to continue to look the same in different projects, rather than taking different approaches. This tool can't be gofmt, as correct handling requires type checking which gofmt does not do. It could be an updated version of go fix. See also modernizers.

We will have to update the go/ast package to support the use of ?, and we will have to update all packages that use go/ast to support the new syntax. That is a lot of packages.

We will also have to update the introductory documentation and the tour. And, of course, existing Go books will be out of date and will need updating by their authors. The change to the language and compiler is the easiest part of the work.

Possible extensions

These are some possible extensions to the above proposal. These are not part of this proposal, but suggest ways that the language could be developed if this proposal seems useful.

Permit return …, err

This proposal works well with proposal #21182 which permits return …, err to return all zero values other than a final error value. For example,

// OpenOrCreate opens a file, creating it if necessary, and returns the
// file's modification time.
func OpenOrCreate(s string) (*os.File, time.Time, error) {
	f := os.OpenFile(s, os.O_CREATE|os.O_RDWR, 0o666) ?
	fi := f.Stat() ? {
		f.Close()
		return ..., err // instead of "return nil, time.Time{}, err"
	}
	return f, fi.ModTime(), nil
}

That said, if we know that err != nil, then writing err ? is the same as writing return …, err (ignoring the details of named result parameters). We could use err ? in the above example. It would require some analysis to see how often return …, err would be useful when we don't know whether or not err is nil.

Permit using ? with no block outside of a function

We could permit using ? with no block outside of a function, by having it implicitly call panic(err). For example:

// fileContents is a package-scope variable that will
// be set to the contents of the file.
// If os.ReadFile fails, the program will panic
// at initialization time.
var fileContents = os.ReadFile("data") ?

I don't think this comes up often enough to be worth doing. Note that this proposal permits

var fileContents = os.ReadFile("data") ? {
	panic(fmt.Sprintf("could not read data file: %v", err))
}

Permit using ? with no block in a test

We could permit using ? with no block in a test function, by having it implicitly call t.Fatal(err). For example:

func TestSomething(t *testing.T) {
	f := os.ReadFile("data") ?
}

I'm not fond of this because it means that we have to somehow recognize test functions in the language. This proposal does already permit

func TestSomething(t *testing.T) {
	f := os.ReadFile("data") ? {
		t.Fatal(err)
	}
}

or, for that matter,

func TestSomething(t *testing.T) {
	testSomething(t) ? {
		t.Error(err)
	}
}

func testSomething(t *testing.T) error {
	f := os.ReadFile("data") ?
	... // other statements that use ? to handle errors
}

Let gofmt retain single line

We could let gofmt retain a ? block on a single line, as in

	r := os.Open(src) ? { return fmt.Errorf("copy %s %s: %v", src, dst, erv) }

This would reduce the error handling boilerplate by 2 newlines, and keep all error handling indented.

I am not in favor of this myself but I know that people will suggest it.

@gabyhelp's overview of this issue: #71203 (comment)

@ianlancetaylor ianlancetaylor added LanguageChange Suggested changes to the Go language Proposal LanguageChangeReview Discussed by language change review committee labels Jan 9, 2025
@ianlancetaylor ianlancetaylor added this to the Proposal milestone Jan 9, 2025
@jimmyfrasche
Copy link
Member

If, instead of a block, it took an optional value of type func(error) error that would allow reusing handlers and with #21498 then inline handlers would just be

stmt ? err => {
  return err
}

which is still quite short

@kortschak
Copy link
Contributor

Would OtherFunction(SomeFunction() ?) be accepted where func SomeFunction() (T, error) and func OtherFunction(v T)? Also for _, v := range (SomeFunction() ?) { … } where func SomeFunction() ([]T, error). (both in a returning context)

@dominikh

This comment was marked as off-topic.

@ianlancetaylor
Copy link
Member Author

@kortschak

Would OtherFunction(SomeFunction() ?) be accepted where func SomeFunction() (T, error) and func OtherFunction(v T)? Also for _, v := range (SomeFunction() ?) { … } where func SomeFunction() ([]T, error). (both in a returning context)

No. The ? is only permitted at the end of an assignment statement or an expression statement.

@kortschak
Copy link
Contributor

@ianlancetaylor Is there a rationale for that that I missed?

@Merovius
Copy link
Contributor

Merovius commented Jan 9, 2025

Personally, I don't feel like the first version (with the block) pays for itself. The visual difference between x, err := f(); if err != nil { … } and x := f() ? { … } just seems pretty small to me, especially with all the indentation. The second version (without the block) on the other hand, is a significant saving and I see the value in that. So I would only add the second version, if it where up to me.

Also, a nit: I find the formatting with the extra space between the rvalue and the ? (i.e. x := f() ? vs. x := f()?) pretty ugly, though I understand that it's probably intentional to make the ? stand out more.

@ianlancetaylor
Copy link
Member Author

@jimmyfrasche

stmt ? err => {
  return err
}

I think that's a little confusing because that return statement is presumably not returning from the short function literal, it's returning from the function that is calling that function literal.

@ianlancetaylor
Copy link
Member Author

Is there a rationale for that that I missed?

No, I didn't write anything about that topic. The rationale is that I don't think a return should occur in the middle of an expression. I think that is harder to understand, especially as we heard toward cases like A(B() ?, C() ?). I think that was one of the difficulties with the try proposal.

@kortschak
Copy link
Contributor

I think that's fair for the calls, but what about the range case (or thinking about it now, switch with .(type); there is no ambiguity there as it's essentially just sugar for the assignment and then loop.

@gophun
Copy link

gophun commented Jan 9, 2025

So I would only add the second version, if it where up to me.

Introducing a special syntax solely for the undecorated (in many cases discouraged) error return might make it appear disproportionately more appealing than the preferred, decorated method of handling errors.

@ianlancetaylor
Copy link
Member Author

@kortschak I suppose I'm trying to keep it as simple as possible, and also I want to ensure that it's always reasonably easy to return an annotated error, rather than biasing even more heavily in favor of an unannotated error (as @gophun also mentions).

@kortschak
Copy link
Contributor

Yeah, I'm just trying to scope out what the intention is here. If it's just assignment, it doesn't really seem worth it to me.

@ianlancetaylor ianlancetaylor added the error-handling Language & library change proposals that are about error handling. label Jan 9, 2025
@gaal
Copy link

gaal commented Jan 9, 2025

Issue #21161 says,

I'm writing this proposal mainly to encourage people who want to simplify Go error handling to think about ways to make it easy to wrap context around errors, not just to return the error unmodified.

The current proposal helps a great deal with this. I would like to describe a possible extension. The idea isn't to hijack the current discussion -- this can certainly be added at a later time. I just want to show how this goal can be furthered.

It is exceedingly common for the error handler to consist only in a return statement. The "..." proposal #21182 is meant to declutter that statement. But I feel like we have the opportunty to bring these ideas together and make all of return, ..., fmt.Errof all unnecessary in the common case.

The extension is to allow ? to be followed by what amounts to arguments to fmt.Errorf, with return ... (in the sense #21182) of inserted before them.

For example, eight lines in debug/elf/file.go:

	data, err := symtabSection.Data()
	if err != nil {
		return nil, nil, fmt.Errorf("cannot load symbol section: %w", err)
	}
	// ...

	strdata, err := f.stringTable(symtabSection.Link)
	if err != nil {
		return nil, nil, fmt.Errorf("cannot load string table section: %w", err)
	}

Can become:

	data := symtabSection.Data() ? "cannot load symbol section: %w", err
	// ...

	strdata := f.stringTable(symtabSection.Link) ? "cannot load string table section: %w", err

Six lines in net/http/transport_test.go become:

	io.CopyN(f, rand.Reader, nBytes) ? "failed to write data to file: %v", err
	f.Seek(0, 0) ?                     "failed to seek to front: %v", err)

(They would also have to have a block, and return nil, nil, fmt.Errorf, without this extension.)

The main disadvantage of this extension is that it adds a dependency on package fmt in code that uses it, and long lines will probably have to be discussed as well. If the current issue is accepted I'll file a separate proposal.

@jrick
Copy link
Contributor

jrick commented Jan 9, 2025

What is the order of assignment, and are any hidden temporary variables used when handling a non-nil error?

That is, would this:

func foo() (int, error) {
	return 1, errors.New("error")
}

var x int
x = foo() ? {
	// nothing
}
fmt.Println(x)

output 0 or 1?

Or, if dereferencing a pointer during the assignment:

var x *int
*x = foo() ? // returns the error
// not reached

would this create a temporary variable for the first return value and avoid the nil dereference when it takes the error path?

@ianlancetaylor
Copy link
Member Author

@jrick Good questions. I think that when there is a block, the assignment should complete before the block is executed. When there is no block, the assignment should not be done. Those seems likes the least confusing choices.

@jrick
Copy link
Contributor

jrick commented Jan 9, 2025

My only concern there is that by refactoring and adding additional context/wrapping to the error in an error block would now perform the nil dereference and crash where it would not have before.

@ianlancetaylor
Copy link
Member Author

@jrick That is true, but it seems to me that if the block refers to the variable on the left hand side of the assignment, it should see the new value, not the old one. Especially since if the assignment uses := the old value is always the zero value. (Admittedly since the error is not nil the new value is likely to also be the zero value.)

@jrick
Copy link
Contributor

jrick commented Jan 10, 2025

Yes, that makes sense. I thought about perhaps using some trickery where only the assignments that were used by the error block were performed prior to the block running, and all other assignments later, but I don't think it is really possible to perform properly due to the potential of pointer aliasing.

@jimmyfrasche
Copy link
Member

jimmyfrasche commented Jan 10, 2025

@ianlancetaylor

I think that's a little confusing because that return statement is presumably not returning from the short function literal, it's returning from the function that is calling that function literal.

That was not my intent. It would be an ordinary function that is called in the ordinary manner. It's return would be used as the value of the error. x := stmt ? handler would essentially desugar to

x, err := stmt
if err != nil {
  return handler(err)
}

@ianlancetaylor
Copy link
Member Author

@jimmyfrasche I see. But then, is there a way to use ? in functions that don't have an error result? Or to use ? when you don't want to always return? Maybe I'm misunderstanding the suggestion.

@jimmyfrasche
Copy link
Member

The handler let's you transform the error (for example, adding extra context or performing clean up actions). What happens after that is up to ?.

If you want ? to, say, panic in funcs that don't return an error then, in those funcs, it could desugar to

if err != nil {
  panic(handler(err))
}

Personally I think ? should only be allowed in funcs that return an error, at least initially.

? should always return. If you don't want that if err is fine and sticks out more. It doesn't need to cover every case, just the common one.

@DeedleFake
Copy link

DeedleFake commented Jan 10, 2025

@jimmyfrasche's suggestion would also solve the problem of err just being magically declared in the block. The pattern it allows would look quite nice, actually.

package fmt

func Handlef(fmt string, args ...any) func(error) error {
  return func(err error) error {
    // Maybe something a bit fancier to allow the error to be anywhere in the format string.
    args = append(args, err)
    return fmt.Errorf(fmt, args...)
  }
}
package example

func Example() error {
  Start() ? fmt.Handlef("start failed: %w")
  Run() ? fmt.Handlef("run failed: %w")
  return nil
}

It's a much less complicated version of #69734.

@doggedOwl
Copy link

doggedOwl commented Jan 10, 2025

Personally, I don't feel like the first version (with the block) pays for itself.

For me it pays fully, because it is not only about token reduction like many make it to be but the main problem many have with the current error handling is the interleaving of the main flow with the error flow that makes reading, not writing, harder.
This proposal solves that by giving a clear separation that makes reading new code and the main flow much more clear and gives flexibility to have explicit returns when needed.
Even if it wouldn't reduce any tokens. it's worth.

@Merovius
Copy link
Contributor

Merovius commented Jan 10, 2025

@doggedOwl I wasn't talking about token-reduction - on the contrary. To me, both forms simply look almost the same, despite one having fewer tokens. So I agree that token-reduction isn't a good metric.

@ncruces
Copy link
Contributor

ncruces commented Jan 25, 2025

If the ? operator always caused control flow to diverge (for example the block has an invisible return .., err at the end) then this change would help both make code shorter, and make it harder to mess up error handling.

This is a good point I hadn't considered.

I proposed the opposite: making return required in the blockless version to solve issue 4, but I guess preventing fall through from the block would also improve clarity.


Also, looking at the above changes, while the blockless version often reads nicely in simple cases, returns do become invisible (which may be the point, but IMO is out of character for Go).


One more, are these really an intentional consequence of the proposal as specified in the original comment?

// blockless
err ?

// blocky
err ? {
  …
}

I ask because, one I personally dislike them, but also because err on its own is not a valid expression statement, whereas call() that returns error is.

And the proposal says:

This section presents the formal proposal.

An assignment or expression statement may be followed by a question mark (?).

@FiloSottile
Copy link
Contributor

FiloSottile commented Jan 25, 2025

I expanded every file in CL 644076 under crypto/internal/fips140 and scrolled through them.

src/crypto/internal/fips140/aes/gcm/cast.go

b := aes.New(key) ?

src/crypto/internal/fips140/aes/gcm/gcm_nonces.go

g := newGCM(&GCM{}, cipher, gcmStandardNonceSize, gcmTagSize) ?

The above feel like an improvement, assuming editors will know to highlight ? when clicking on a return. I find the option to highlight all exit points critical in understanding code, and the ? is a smaller target, but will probably still be visible since it's at the end of the line.

Thinking of this though made me realize that ? is only an exit point if not followed by a block, so we could still make the following classic mistake. This feels like a missed opportunity for something that the one big change to error handling should fix.

foo() ? {
    http.Error(e, err.Error(), 500)
}

src/crypto/internal/fips140/ecdh/ecdh.go

drbg.ReadWithReader(rand, key) ?

src/crypto/internal/fips140/ecdsa/ecdsa.go

drbg.ReadWithReader(rand, Z) ?

This is really scary. The difference between a major security vulnerability and secure code is the lone ?.

src/crypto/internal/fips140/ecdsa/cast.go

	got := sign(P256(), k, drbg, hash) ?
	verify(P256(), &k.pub, hash, got) ?

This got a lot prettier, but see above for the load bearing ? after verify.

If this is accepted, I might need to start using errcheck. Maybe that's an ok tradeoff.

src/crypto/internal/fips140/ecdsa/ecdsa.go

func randomPoint[P Point[P]](c *Curve[P], generate func([]byte) error) (k *bigmod.Nat, p P, err error) {
	for {
		b := make([]byte, c.N.Size())
		generate(b) ? {
			return nil, nil, err
		}

This might be a tooling issue, maybe due to generics? Why the block? If it's actually required, then there's something I don't get about the proposal.

src/crypto/internal/fips140/ecdsa/ecdsa.go

func verifyGeneric[P Point[P]](c *Curve[P], pub *PublicKey, hash []byte, sig *Signature) error {
	// FIPS 186-5, Section 6.4.2

	Q := c.newPoint().SetBytes(pub.q) ?
	r := bigmod.NewNat().SetBytes(sig.R, c.N) ?

	if r.IsZero() == 1 {
		return errors.New("ecdsa: invalid signature: r is zero")
	}
	s := bigmod.NewNat().SetBytes(sig.S, c.N) ?

	if s.IsZero() == 1 {
		return errors.New("ecdsa: invalid signature: s is zero")
	}

	e := bigmod.NewNat()
	hashToNat(c, e, hash)

	// w = s⁻¹
	w := bigmod.NewNat()
	inverse(c, w, s)

	// p₁ = [e * s⁻¹]G
	p1 := c.newPoint().ScalarBaseMult(e.Mul(w, c.N).Bytes(c.N)) ?

	// p₂ = [r * s⁻¹]Q
	p2 := Q.ScalarMult(Q, w.Mul(r, c.N).Bytes(c.N)) ?

	// BytesX returns an error for the point at infinity.
	Rx := p1.Add(p1, p2).BytesX() ?

	v := bigmod.NewNat().SetOverflowingBytes(Rx, c.N) ?

	if v.Equal(r) != 1 {
		return errors.New("ecdsa: signature did not verify")
	}
	return nil
}

Definitely improved. Post-proposal I would probably have made a NotZero helper that returns an error if zero, so that even those two first if would fold. Maybe I would have also made Equal return an error. It's interesting how this syntax encourages picking a side for boolean functions and declaring it the "error" side.

src/crypto/internal/fips140/ed25519/ed25519.go

fipsPCT(priv) ?
// This can happen if the application messed with the private key
// encoding, and the public key doesn't match the seed anymore.

Tooling issue, should not move a comment from inside the block to below the statement.

src/crypto/internal/fips140/ecdsa/ecdsa.go

func inverse[P Point[P]](c *Curve[P], kInv, k *bigmod.Nat) {
	if c.ordInverse != nil {
		kBytes, err := c.ordInverse(k.Bytes(c.N))
		// Some platforms don't implement ordInverse, and always return an error.
		if err == nil {
			kInv.SetBytes(kBytes, c.N) ? {
				panic("ecdsa: internal error: ordInverse produced an invalid value")
			}
			return
		}
	}

I have this feeling in a lot of places, but here it's particularly marked because of the deep nesting: it's confusing to see blocks start from lines that don't have a keyword I recognize. Maybe I need to get used to it. But I can scan nested ifs better than nested if and kInv.

src/crypto/internal/fips140/hmac/hmac.go

imarshal := marshalableInner.MarshalBinary() ? {
	return
}

This confused me a lot until I realized this function has no return values. I initially thought maybe it was relying on named returns?

That leads me to a question though: how do named err returns work? The only mention I find in the proposal is

If the ? is followed by a block, and the qvalue is not nil, then the block is executed. Within the block a new variable err is implicitly declared, possibly shadowing other variables named err. The value and type of this err variable will be those of the qvalue.

which doesn't answer the question of how this behaves.

func foo() (err error) {
    err = errors.New("1")
    bar() ? {
        return
    }
}

I think I would find both behaviors surprising.


Overall, I think I found this a strict improvement when there is no block, and at least one other return value. I found this a readability improvement but uncomfortably easy to get wrong when there is no block and no other return value. I found this a loss in readability with little gain in conciseness when the block is there.

@apparentlymart
Copy link

apparentlymart commented Jan 25, 2025

FWIW @ConradIrwin, @ncruces there was some much earlier discussion about forcing a control flow change: #71203 (comment), #71203 (comment). Unfortunately it got lost in the big gap of GitHub hiding most of the discussion, so I guess I can say a little more about it now that we're studying some real examples.

My take on it was a little different in that I wondered if we could/should make it an error for the end of the error-handling block to be reachable (with a potential opt-out of explicitly writing fallthrough at the end) but that any statically-detectable means of preventing control flow from reaching there is acceptable. For example:

// The main case... return the error with some decoration
MightFail()? {
    return fmt.Errorf("reticulating splines: %w", err)
}
// in any kind of loop...
for {
    MightFail()? {
        err = errors.Join(retErr, err) // keep track of potentially-many errors
        continue // ...begin the next iteration of the loop
        // break would also be acceptable, though the use-cases for that are more dubious
    }
}
MightFail()? {
    panic(err) // Compiler knows that panic does not return
}

What this would not allow today is just calling something like os.Exit, log.Fatal, t.Fatal, etc, because the compiler doesn't know that those functions diverge. Someone intending to use one of those would still need to write some sort of terminating statement afterwards to convince the compiler that control is definitely being redirected somewhere else, which is non-ideal. That could potentially be improved by a separate proposal that introduces some way to declare or automatically infer that a particular function does not return, but that's beyond the scope of this proposal.

Personally I think that a rule like this would help this new syntax pull its weight, because it would also help guard against a relatively-common mistake. This, combined with the err symbol being declared only for the error block and not for the parent scope (so there's less shadowing), were what changed this from being just a cute abbreviation into something I think could be genuinely valuable in helping people write clearer and more robust code.

@chad-bekmezian-snap
Copy link

One thing that I just realized would be possible, unless I am missing something, is:

func Foo() (int, error)
func MinusOne(v int) (int)

func main() {
   println(MinusOne(Foo() ?))
}

I can see this getting pretty confusing to follow in nested function calls.

@apparentlymart
Copy link

apparentlymart commented Jan 25, 2025

@chad-bekmezian-snap What you've shown is not allowed by the proposal. The ? symbol is only allowed at the end of assignment statements and expression statements.

Some earlier discussion about that:

@ncruces
Copy link
Contributor

ncruces commented Jan 25, 2025

@chad-bekmezian-snap What you've shown is not allowed by the proposal. The ? symbol is only allowed at the end of assignment statements and expression statements.

How does that square with:

err ? {
  …
}

A standalone err should not be an expression statement.

@apparentlymart
Copy link

apparentlymart commented Jan 25, 2025

@ncruces that did surprise me too (in #71203 (comment)) since I'm accustomed to the compiler complaining if a statement consists entirely of a variable name, but the spec for expression statements says nothing about that being invalid.1

Instead, it seems that what makes this normally an error is a check that the variable is "used". And in Ian's current implementation of the proposal, adding the ? after it seems sufficient for it to be "used" and therefore valid. Whether that's a desirable property is of course still up for some debate.

Footnotes

  1. It's interesting that the BNF definition of ExpressionStmt seems, at least to my unfamiliar eye, more permissive than the text above it: the text says that function calls, method calls, and receive operations are allowed "in statement context", but it doesn't say that any other expression types are disallowed except for the specific built-in functions listed, and the BNF just says that it's the same as the Expression production, which (if you follow indirection a few times) does include OperandName, which matches err. I'm therefore not sure if the spec author intended err to be a valid expression statement, but Ian seems to have decided yes for the sake of this proposal.

@ncruces
Copy link
Contributor

ncruces commented Jan 25, 2025

I disagree that it doesn't:

With the exception of specific built-in functions, function and method calls and receive operations can appear in statement context. Such statements may be parenthesized.

The only expressions that can be statements are “(1) function calls, (2) method calls, (3) and receive operations” (with the exception of some built-in function calls, basically, the ones that have no effect if you don't use the result).


Not to add to the notification spam, and further bury comments. I agree with you about @chad-bekmezian-snap's concern.

I'm more concerned that the tool is applying an automatic modification that does not seem covered by the proposal as specified, and would like to know if it's intentional.

@apparentlymart
Copy link

apparentlymart commented Jan 25, 2025

Well, notwithstanding the current specification text Ian has been pretty clear that this proposal is not intended to allow ? to appear inside of another expression.

Exactly how the specification text would be modified to define it in that way remains to be seen if this proposal is accepted, but nonetheless it's been made explicit by the author that the proposal does not intend to allow the usage that @chad-bekmezian-snap was concerned about, and the concern about it was already raised far earlier in the thread so we don't seem to be adding any new information here.

@chad-bekmezian-snap
Copy link

Well, notwithstanding the current proposal text Ian has been pretty clear that this proposal is not intended to allow ? to appear inside of another expression.

Exactly how the specification text would be modified to define it in that way remains to be seen if this proposal is accepted, but nonetheless it's been made explicit by the author that the proposal does not intend to allow the usage that @chad-bekmezian-snap was concerned about, and the concern about it was already raised far earlier in the thread so we don't seem to be adding any new information here.

Ah apologies for missing that. Thanks for the correction.

@eihigh
Copy link

eihigh commented Jan 25, 2025

While this may contradict my previous stance, I believe this proposal offers the most robust and straightforward error handling solution when combined with a linter.

Historically, in many Go codebases, errors were often ignored when the return value wasn't needed and errors were rare, because writing if err != nil { return nil, err } felt too cumbersome. However, now that error propagation requires just adding a ? at the end of a line, I believe it's reasonable for linters to warn about unhandled errors for all error-returning functions:

f := os.Open(name) ?
defer f.Close() ? // warn if ? is absent

Similarly, I think linters can effectively address the concern about incorrectly terminating error handling blocks without a return statement. Linters can be configured to exclude special cases like t.Fatal or os.Exit, making them a practical solution for this issue.


Additionally, I'd like to share my thoughts on readability, though this isn't part of the formal discussion.

I understand that many developers are frustrated by how error handling code intermingles with normal control flow. In the current approach, while having err := scattered horizontally isn't too problematic, the vertical noise from if err != nil { } blocks (which often grow longer than the happy path) significantly impacts readability.

While check-handle and builtin try function proposals reduce vertical noise, they don't address horizontal clutter. The try approach particularly suffers from having two visual elements (try( and )) that impair readability.

This proposal, however, makes a clear horizontal separation between normal flow (at the beginning) and error handling (at the end), significantly improving readability.

While other proposals that place tokens like try at the line start also reduce horizontal noise, this proposal proves superior when considering factors like the ease of adding additional error handling code.

@ncruces
Copy link
Contributor

ncruces commented Jan 25, 2025

defer f.Close() ?

Is this even covered by the proposal? Would it work? If it does, what does it do?

Because both straightforward interpretations of it would be wrong in most situations.

Usually, you want to return the error from a defered function if and only if you're not already returning an error.

@godcong
Copy link

godcong commented Jan 26, 2025

defer f.Close() ?

? Here I think it's necessary

A lot of Close() has an odd design.
It returns error, but defer doesn't do anything about it.
Here I think that if the error is not absorbed defer should throw panic
If absorbed is equivalent to _ = Close()

@jellevandenhooff
Copy link
Contributor

For the common case of a function that returns a single value or an error, this proposal would support an annotation trick similar to error wrapping in Rust with anyhow:

func inner() (string, error) {
        ...
}

func foo() error {
	result := try(inner()).context("inner: %w") ?
	fmt.Printf("result: %s", result)
	return nil
}

which would desugar to

func foo() error {
	result, err := inner()
	if err != nil {
		return fmt.Errorf("inner: %s", err)
	}
	fmt.Printf("result: %s", result)
	return nil
}

This works with a generic function try:

type tryer[A any] struct {
	a A
	b error
}

func (t tryer[A]) context(f string) (A, error) {
	if t.b != nil {
		t.b = fmt.Errorf(f, t.b)
	}
	return t.a, t.b
}

func try[A any](a A, b error) tryer[A] {
	return tryer[A]{a: a, b: b}
}

@AndrewHarrisSPU
Copy link

In #71203 (comment) Ian ran a tool to give a preview of ? applied to the standard library. I looked at a lot of (properly random) files, and some conclusions I think I take away:

  1. A blockless-? conversion is almost always worthwhile, but shouldn't always be demanded.

  2. With block-?:

  • Less tokens is good, but the fewest tokens possible is not always better than fewer.
  • I'm more convinced that, rather than implicit assignment, the sanest direction to ask for an identifier for the trailing error with block-? form. It felt really risky in situations with nested control flow. I'm not sure a predeclared identifier would help here - it's not saving tokens, and it's new rules intermingling with familiar usage.
  1. err ? is good, actually. No, really - I thought it was jarring at first but the more I think about it, err ? is great, especially if code authors are given a little breathing room to use err ?.
  1. err ? helps when lines are dangerous or long, or the ? follows a multiline statement.
err := drbg.ReadWithReader(rand, Z)
err ?
  • long:
r, err := SomeFunction(enterprise.FizzBuzz(enterprise.GeneratorFunc(3), enterprise.GeneratorFunc(5), enterprise.LogThreeAndFive("fizz", "buzz")))
err ?
  • multiline:
r, err := SomeFunction(&FooBarBaz{
	Foo: "foo",
	Bar: "Bar",
	Baz: "Baz",
})
err ?
  1. err ? stands out against other conditionals

Consider src/bufio/bufio_test.go:941, with a slight amount of additional rewriting to flatten the conditional tree:

		for {
			line, isPrefix, err := l.ReadLine()
			if len(line) > 0 && err != nil { // condition 1
				t.Errorf("ReadLine returned both data and error: %s", err)
			}
			if isPrefix { // condition 2
				t.Errorf("ReadLine returned prefix")
			}
			if err == io.EOF { // condition 3
				break
			}
			err ? { // condition 4
				t.Fatalf("Got unknown error: %s", err)
			}

			if want := testOutput[done : done+len(line)]; !bytes.Equal(want, line) { // conditional 5
				t.Errorf("Bad line at stride %d: want: %x got: %x", stride, want, line)
			}
			done += len(line)
		}

It's subtle, but note in this listing that there's a total ordering of conditionals. Further, that ordering is try-catch-finally followed by test logic. It's useful that err ? marks the finally part.

  1. err ? stands out when a short amount of essential work is done in between the call.

src/cmd/go/internal/mvs/mvs.go:137 has a pretty compelling example - grabbing a mutex is short but essential work:

		mu.Lock()
		err ? {
			errs[m] = err
		}
		if u != m {
			upgrades[m] = u
			required = append([]module.Version{u}, required...)
		}
		g.Require(m, required)
		mu.Unlock()
  1. ? usage in test code is worth examining

In test code, ? helps. More help is latent/unexplored in existing tools than is given by ? (which is fine IMHO - test code is a little different).

  1. The qmod tool seemed more questionable on test files. It makes me think there shouldn't be any urgency to update them.

  2. I was surprised, tabulating occurences of ?, by how many could be categorized as "block-?, test control flow". My counts had it as the most frequent by 3x, where blockless-? or other block-? usage was balanced.

The question this provokes is, is there's an unmet need for something more than the basic proposal? E.g.: an implicit t.Fatal, or something with error->error functions (#71203 (comment))?

I'm not sure, but I'm leaning towards thinking the basic proposal is fine. We can do other things to use less tokens, and the fewest tokens possible is not necessarily better.

fail := func(err error) {
	err ? {
		t.Fatal(err)
	}
}

...

r, err := SomeFunction("test1.txt")
fail(err)

...

@ncruces
Copy link
Contributor

ncruces commented Jan 26, 2025

Nothing prevents you from doing this today.

fail := func(err error) {
	if err != nil {
		t.Fatal(err)
	}
}

...

r, err := SomeFunction("test1.txt")
fail(err)

Is err != nil once in a helper a show stopper? Why don't you/we do this today?

@AndrewHarrisSPU
Copy link

Nothing prevents you from doing this today.

Oh, yeah, I already do this sometimes. I'm not trying to suggest the helper is easier to write, but that it's possible already, as you note.

To restate my point: ? is not highly solvent for streamlining error handling in existing test code, but accounts for a ton of churn caused by qmod. It's not clear to me that maximal streamlining of error handling in test code is always even the right priority. And if it is, this proposal isn't exactly it.

@ianlancetaylor
Copy link
Member Author

@josharian Thanks for the detailed look.

Regarding #71203 (comment) about cmd/api/api_test.go, I assuming that you are asking about the introduction of var err error. Yes, that is a shortcoming of the tool. It isn't currently able to work out that because the next use of err is in a := assignment that it doesn't need to introduce a declaration to replace the one that it introduced.

The change in src/cmd/fix/typecheck.go:160 looks pretty undesirable. But again, probably no human would make that change?

I agree. That is a valid change according to this proposal but not one that I expect a human to write.

Changes purely from if err != nil { to err ? {, as in src/cmd/go/internal/lockedfile/internal/filelock/filelock_unix.go, definitely feel like they don't pull their weight.

Agreed. This falls out of the proposal but I don't expect it to be widely used.

However, to my eyes, src/cmd/go/internal/lockedfile/lockedfile.go line ~112 shows a real downside of having the ? be at the end of the line.

f := OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) is really long. It's easy to lose the ? at the end, vs seeing the err parameter near the beginning. (For this reason, I personally rarely use if x, err := f(); err != nil {, despite the scoping advantages.)

I understand that perspective but I don't entirely agree. I think that in ordinary reading we can assume that this kind of call will succeed. When we are concerned that it might fail, we will know to look to the end of the line. That is, I think this proposal makes this kind of code easy to read for the normal case.

@ianlancetaylor
Copy link
Member Author

@eihigh

Is this intentional that there's no unused variable compiler error for err, even though it's not used in this Go code?

That is a good question and I don't have a definite answer. My inclination is to say that it's OK to not reference err, because there is in a sense an implicit test of err != nil just to get to this code. That is, we are not ignoring the error value even if we don't use it. But I could be convinced otherwise.

@ianlancetaylor
Copy link
Member Author

@ConradIrwin

When writing code it is too easy to forget to actually return the error, and this change makes that worse.

Noted. Interesting example. Thanks.

@ianlancetaylor
Copy link
Member Author

@FiloSottile

Thanks for the detailed look.

This might be a tooling issue, maybe due to generics? Why the block? If it's actually required, then there's something I don't get about the proposal.

It's a tooling issue due to generics. The tool doesn't recognize that nil is the zero value for the type parameter.

func foo() (err error) {
    err = errors.New("1")
    bar() ? {
        return
    }
}

In the current proposal that would discard the error returned by bar and return errors.New("1"). Interesting example.

@ianlancetaylor
Copy link
Member Author

ianlancetaylor commented Jan 27, 2025

@ncruces

This proposal does not permit

    defer f.Close() ?

The defer statement requires a function call, and while f.Close() is a function call, f.Close() ? is not.

EDIT: the rest of this was wrong, see #71203 (comment)

This proposal would permit

defer func() { f.Close() ? }()

which would have an effect roughly similar to the following when used in a function with a named err result:

defer func() {
if err1 := f.Close(); err1 != nil {
err = err1
}
}()

@DmitriyMV
Copy link
Contributor

DmitriyMV commented Jan 27, 2025

@ianlancetaylor having ? always diverge control flow would make it actually quite similar to the Rust .map_err(...)? which will ease the movement back and forth between two languages. It could be also explained to the new users that they can think of the block after ? as an implicit lambda.

This proposal would permit

    defer func() { f.Close() ? }()

which would have an effect roughly similar to the following when used in a function with a named err result:

    defer func() {
        if err1 := f.Close(); err1 != nil {
            err = err1
        }
    }()

What happens if the function result error is already assigned by the code in the function body?

@ncruces
Copy link
Contributor

ncruces commented Jan 27, 2025

@ianlancetaylor

This proposal would permit

    defer func() { f.Close() ? }()

I'm starting to get the feeling that I either don't understand the language or the proposal. From the proposal:

The block may be omitted if the statement using ? appears in the body of a function, and the enclosing function has at least one result, and…

The deferred function, on which ? is used, has no result. So how can a blockless ? be used there?

Is the key here the word “enclosing”? If so, how does that square with multiple levels of nesting? Does the ? always immediately return from the outermost function call? If so that's a significant control flow change, as there is no other way in the language to do that; “enclosing” carries a lot of weight in that sentence.

Also, as I pointed out, that rough desugaring is not what people typically want. We make spaghetti out of errors in defer because we only want the deferred error when the function is not already returning an error.

I would also again ask for clarification of the following case, because although err seems to match the EBNF of “expression statements” it does not match the plain english description of them:

err ? {
  …
}

An isolated err cannot usually be used as a statement, even if the check that trips this is evaluated later by the compiler (not the parser, but some later stage that checks for statements with no effect).

Sorry if I'm out of my depth.

@mitsuhiko
Copy link

The deferred function, on which ? is used, has no result. So how can a blockless ? be used there?

It modifies the "err" variable on the outer function in the same way as a defer could manipulate a named return:

func x() (i int) {
	defer func() { i = 1; }()
	return i;
}

@jfgiorgi
Copy link

jfgiorgi commented Jan 27, 2025

I don't like the "explicit err declaration" part. it's confusing and not natural (or "err" must be a token/reserved word of the langage like "self" in some OO language.)
I'd rather have a new token/operator used in the language to "decorate" functions to indicate they return an extra unamed Error value.
For instance the token ! (or whatever is easier to implement in the lexer/parser)
Then use that Error value in this "?" proposal.

(I haven't read all proposals and comments, so sorry if this has already been proposed or it's out of subject).

func SomeFunc(src, dst string) (int,int,error)

becomes equivalent to

func SomeFunc!(src, dst string) (int,int)

"!" can be used to access the error in the ? scope block:

i,j := SomeFunc!("a","b") ? {
    fmt.Printf("error occured: %s", !)
    // handle the error
}
more details

declaring:

func SomeFunc!(src, dst string) (int,int) {
  if src == "" {
      return ! fmt.Errorf("bad src") // default "zero" values for the return values
  }
  if dest == "" {
      return 2, 3 ! fmt.Errorf("bad dst") // explicit values for the return values
  }
  return 4, 5 // nil  error
}

function with no return value(s) and bubbling up error:

func Other!(src string)  {
  a,b := SomeFunc!(src,"hello") ? // direct bubble up
  if a+b > 4 {
    return ! fmt.Errorf("bad sum")
  }
  c,d := SomeFunc(src,"world") ? { // wrapping
    return ! fmt.Errorf("got %w",!)
  }
  // nil error
}

explicitly getting the error outside the ? scope:

var err Error
i,j := SomeFunc!("a","b") ? {
    err = !
}
// handle err

multiple & nested errors:

func Positive!(i int) int {
  if i == 0 {
    return ! fmt.Errorf("zero is bad")
  }
  if i < 0 {
    return ! fmt.Errorf("negative")
  }
  return i
}

t := Positive!(1) + Positive!(-1) + Positive!(Positive!(0)) ? {
   // if at least one error occured, we're in this scope
   // here "!" as type []error so much like errors.Unwrap()
}

ps: to avoid too many '!' in code, it can be omitted when calling a function (granted type checking is ok).

@ncruces
Copy link
Contributor

ncruces commented Jan 27, 2025

It modifies the "err" variable on the outer function in the same way as a defer could manipulate a named return:

func x() (i int) {
	defer func() { i = 1; }()
	return i;
}

func() { i = 1; }() here is just an anonymous function. It doesn't return an error. If it did, the returned error would be ignored by defer (which is the entire issue with defer f.Close().

Also, a return statement statement inside an anonymous function returns from the anonymous function. There's no way to return from x in your example from inside an anonymous function (deferred or not).

The specification for the proposal does not special case deferred functions at all. It doesn't even mention anonymous functions/literals. So I fail to see how a ? inside a (possibly deferred) anonymous function, with or without a block, can be used to return from the outer function.

If there's a goal here to simplify error handling within deferred functions, that's great actually. Because that is a pain point, and something many (myself very much included) don't handle correctly in many, many (most?) cases, because every solution is a contrived mess. I put this at the level of fixing for loops, not at the if err != nil { all the time is slightly annoying.

@ianlancetaylor
Copy link
Member Author

@ncruces My apologies, you're quite right, and I was wrong.

The proposal does not permit either

     defer f.Close() ?

or

    defer func() { f.Close() ? }()

It does permit

    defer func() error { f.Close() ? }()

but that is useless—it's basically the same as defer f.Close().

@ianlancetaylor
Copy link
Member Author

Thanks for all the feedback so far. It's very helpful.

I've opened a discussion for this proposal: #71460 (I should probably have started this as a discussion). I've started some initial comment threads for emoji voting. Let's move any further comments over to that discussion. I'm going to close this issue.

@ianlancetaylor ianlancetaylor closed this as not planned Won't fix, can't repro, duplicate, stale Jan 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
error-handling Language & library change proposals that are about error handling. LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Projects
None yet
Development

No branches or pull requests