go: proposal: check - a minimal error handling improvement for readability

Author: I have been programming in Go since before the 1.0 release, have also used C/C++, java and python and other languages professionally.

Background: There have been many proposals regarding error handling, such as https://github.com/golang/go/issues/32437 which this proposal have some similarities with. Most Go programmers agree that something could be done to improve error handling, but so far no proposal has been accepted.

What makes this proposal different: In my opinion, current error handling in Go have these 3 problems:

  1. verbose
  2. err variable pollutes the local namespace
  3. happy path and error handling are interleaved, making code (IMO) less readable

Most error handling proposals have focused on solving problem 1 - this proposal aims to solve all 3 problems. If this proposal is not accepted (statistically likely given the history of error handling proposals 😃 I at least hope that it will influence other proposals to pay more attention to problem 2 and 3.

Example: Let’s take this example code which IMO illustrates the 3 problems mentioned above:

func readFile(name string) ([]byte, error) {
	f, err := Open(name) // err unnecessarily pollutes local namespace
	if err != nil { // can't indent this line
		return nil, err
	}
	defer f.Close()

	contents, err := readContents(f) // err variable is reassigned a new value
	if err != nil { // can't indent this line
		return nil, fmt.Errorf("error reading %s: %w", name, err)
	}

	// the validate call is in the middle of a lot of boilerplate, impacting readability
	// here a new err variable shadows the existing one, a potential for confusion/bugs
	if err := validate(contents); err != nil { 
		return nil, err
	}

	return contents, nil
}

Here is how the code would look like with this proposal accepted:

func readFile(name string) ([]byte, error) {
	f := check Open(name)
	defer f.Close()

	contents := check readContents(f); err {
		return nil, fmt.Errorf("error reading %s: %w", name, err)
	}

	check validate(contents)

	return contents, nil
}

Further discussion: This proposal aims to:

  1. mimimize boilerplate
  2. avoid polluting local namespace with error variables
  3. keep the happy path to the left of each line, move error handling to indented blocks to improve readability
  4. the ā€œcheckā€ keyword still makes it clear where the function can return
  5. keep it easy to handle errors when necessary, such as logging or wrapping them

Details: This is obviously not up to language spec standard, but I have tried to address all the specific details I could think of:

  • The check keyword can only be used as a top-level statement in a code block or on the right hand side of a assignment statement.

  • The check keyword is followed by a function call which has a last return parameter of type error OR an expression which has type error.

  • The resulting values of a check statement on a function call is the remaining return parameters without the last error return parameter.

  • If the check statement is applied to an expression with type error there is no resulting values and it is a compile error to try to use it on the right side of an assignment statement.

  • The function call/expression can be followed by a semicolon, a variable name and a code block. If the error value that was extracted by the check statement is not nil, the variable name will be assigned the error value and the code block will be executed. The variable is local to the check statements code block. If the error value is nil, no assignment will happen and the code block is not executed.

If the check statement does not have a code block then it behaves like this:

check f(); err {
	return ..., err
}

Where … is replaced with the zero values of the remaining return types of the enclosing function. If a return parameter in … is a named returned parameter, the current value of that parameter is returned.

Omitting the error handling block is only allowed inside a function which has a last return parameter of type error.

Alternative 1a/b: Check could be replaced with ā€œtryā€. The semicolon could be replaced with else.

func readFile(name string) ([]byte, error) {
	f := try Open(name)
	defer f.Close()

	contents := try readContents(f) else err {
		return nil, fmt.Errorf("error reading %s: %w", name, err)
	}

	try validate(contents)

	return contents, nil
}

Alternative 2: Alternatively a postfix operator could be used instead of the ā€œcheckā€ keyword:

func readFile(name string) ([]byte, error) {
	f := Open(name)?
	defer f.Close()

	contents := readContents(f)? err {
		return nil, fmt.Errorf("error reading %s: %w", name, err)
	}

	validate(contents)?

	return contents, nil
}

Alternative 3: Put the check keyword first on the line to simplify the grammar and maximize clarity of where the function can return.

func readFile(name string) ([]byte, error) {
	check f := Open(name)
	defer f.Close()

	check contents := readContents(f); err {
		return nil, fmt.Errorf("error reading %s: %w", name, err)
	}

	check validate(contents)

	return contents, nil
}

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 18
  • Comments: 22 (8 by maintainers)

Most upvoted comments

I like the implicit non nil checks with the explicit return of the error from your proposal. I understand why you write the declaration of err but I think this could still be part of the normal variable declaration. The “check“ could be tied to the variable instead of the function call, indicating that the variable will be non nil checked

The following would be a ā€˜smaller’ language change, could work with other types than error and would still fullfill most of the goals of this proposal:

func readFile(name string) ([]byte, error) {
    f, check err := Open(name) {
        return nil, err
    }
    defer f.Close()

    contents, check err := readContents(f) {
        return nil, fmt.Errorf("error reading %s: %w", name, err)
    }

    // the validate call is in the middle of a lot of boilerplate, impacting readability
    // here a new err variable shadows the existing one, a potential for confusion/bugs
    check err := validate(contents) { 
        return nil, err
    }

    return contents, nil
}

Since it is impossible to confuse assignments and comparissons in declarations because you cannot compare a not declared variable. This could actually use the existing if keyword instead of check:

func readFile(name string) ([]byte, error) {
    f, if err := Open(name) {
        return nil, err
    }
    defer f.Close()

    contents, if err := readContents(f) {
        return nil, fmt.Errorf("error reading %s: %w", name, err)
    }

    // the validate call is in the middle of a lot of boilerplate, impacting readability
    // here a new err variable shadows the existing one, a potential for confusion/bugs
    if err := validate(contents) { 
        return nil, err
    }

    return contents, nil
}

It seems like the closest current form to what’s being proposed here is the if statement with a ā€œsimple statementā€ appearing before the predicate, like this part of the motivating example:

	if err := validate(contents); err != nil { 
		return nil, err
	}

The form above already confines the err symbol to the body of the if statement, solving your problem statement 2, and the happy/error path interleaving seems the same as your proposal and so I think both this and your proposal are equal under problem statement 3.

I believe the following is the closest equivalent to the above example under your proposal:

	check validate(contents); err {
		return nil, err
	}

This has:

  • Replaced if with check.
  • Made the err != nil predicate implied rather than explicit.
  • Moved the err variable declaration to after the semicolon, replacing the now-implied predicate.

I would agree that there’s less boilerplate in this form than in the if statement form I included above, and therefore it might represent an improvement to problem statement 1: ā€œverboseā€.

A typical reason I’ve heard for not using this if err := ...; err != nil { pattern is that to some readers it ā€œhidesā€ the most important information – the function call – behind some other content. It therefore makes it harder to skim the code. I don’t personally have a strong opinion on that, but it does seem like your proposal reduces the amount of content appearing before the function call, reducing it to just the single keyword check. Perhaps with some time to grow accustomed to it Go readers would find that comfortable to skim.

Based just on the above I don’t feel super convinced this is enough of an improvement to justify the exception in the language. I do agree that it is an improvement, but the improvement feels marginal by this criteria.


I think you’ve buried the lede a bit in your proposal because what you suggested does introduce something new that’s materially different from the typical if statement but that I didn’t quite lock onto until a second read: it allows declaring new symbols both in the parent scope (the non-error results) and the child scope (the error result) without requiring a separate declaration statement.

I missed it on my first read because the example you chose for the if block was a function that only returns an error, but let’s consider how my if statement would look for your readContents example.

I see two main ways to lay it out. The first would be to indent both the happy path and the error path so that neither appears in the parent scope:

	if contents, err := readContents(f); err == nil {
		// happy path using "contents"
	} else {
		// error path using "err"
	}

I’m sure most would agree that the above is not idiomatic because we typically expect the happy path to be unindented and only the error branch to be indented.

The other way is to hoist one of the variable declarations out of the if:

	var contents []byte
	if contents, err := readContents(f); err != nil {
		// error path using "err"
	}
	// happy path using "contents"

Of course in practice I’d probably write this just like you wrote it in your original example, with the whole call on the line above the if statement and only if err != nil, but I’ve written it this way because it’s the closest I can get to meeting your criteria 2 (err only being declared in the nested scope) with today’s Go. And of course this is only true if you exclusively write it this way, so that no other earlier statements have already declared an err in the parent scope.

One thing I quite like about your proposal then is that it quite neatly handles this ā€œscope splittingā€ goal, allowing contents and err to both be declared by the same statement but for err to belong only to the child scope:

	contents := check readContents(f); err {
		// error path using "err"
	}
	// happy path using "contents"

I will say that the exact details of the syntax feel a little awkward to me – it isn’t super clear that the err after the semicolon is effectively a variable declaration – but I think the broad idea is interesting and does add something new to the language, albeit perhaps a relatively small thing. It is at least cosmetically similar to the switch statement form that has a simple statement embedded in it, like switch foo := bar(); foo {, but even in this case that terminal foo is an expression rather than a declaration.

But I think it’s probably more important to discuss what the desirable properties of a solution are than to debate the exact details of syntax, so I won’t quibble too much about the syntax details. If it seems (to others in this discussion) like this ā€œscope splittingā€ behavior is desirable then I’m sure there are various other possible syntax shapes to consider that would make different tradeoffs of terseness vs. similarity to existing language constructs.

I’m also not super convinced that this new capability is sufficiently useful to justify the extra language complexity, but I could imagine myself getting used to this pattern and enjoying it if it were in the language.

Why don’t make it as a directive ? like //go:check {NilType Return Name}, this can accept a wide range and not just errors but any types that can have nil value. in code

func readFile(name string) (content []byte, err error) {
	//go:check err 
        f, err := Open(name)
	defer f.Close()

	content, err = func(fs *File) (f, []byte, err error) {
	        
	        //go:check f, err
                f, err = fs.Body()          

        }
        //go:check err
	err = validate(contents)
	return 
}

Since func() (err error) ā€œerrā€ can either return the default or the value assigned to before //go:check

Regarding how this would be integrated into the language grammar - there are of course several possible ways to do it depending on how many places you would want to allow checks to be used. Given that one of the criticisms of proposal https://github.com/golang/go/issues/32437 was that try could be used in any expression and thus lead to very complicated code my suggestion would be the following:

Statement =
	Declaration | LabeledStmt | SimpleStmt |
	GoStmt | ReturnStmt | BreakStmt | ContinueStmt | GotoStmt |
	FallthroughStmt | Block | IfStmt | SwitchStmt | SelectStmt | ForStmt |
	DeferStmt | CheckStmt .

CheckStmt = [IdentifierList ":="] "check" Expression [";" identifier Block] .

ā€œ;ā€ would be replaced with ā€œelseā€ if that is considered preferable.

From a grammar point of view it would be cleaner to put the check keyword first in the statement to be more similar to ā€œifā€, ā€œforā€ etc… in that case the grammar would be:

CheckStmt = "check" [IdentifierList ":="] Expression [";" identifier Block] .

This is not as clean from a readability standpoint IMO, but it would on the other hand make check always be first on the line so that it becomes as visible as return.

Thanks, appreciate it!

Again the main argument is that this:

func readFile(name string) ([]byte, error) {
	f := check Open(name)
	defer f.Close()

	contents := check readContents(f); err {
		return nil, fmt.Errorf("error reading %s: %w", name, err)
	}

	check validate(contents)

	return contents, nil
}

is more readable than this:

func readFile(name string) ([]byte, error) {
	f, err := Open(name)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	contents, err := readContents(f)
	if err != nil {
		return nil, fmt.Errorf("error reading %s: %w", name, err)
	}

	if err := validate(contents); err != nil { 
		return nil, err
	}

	return contents, nil
}

while also always keeping err variables local to the error handling scope, thanks.