Skip to content

Conversation

thaJeztah
Copy link

Named return values called 'err' are discouraged because they can lead to confusion and subtle mistakes.

In most Go code, 'err' is used for short-lived local variables. Naming a return parameter 'err' can induce cognitive load.

For example:

func fn() (err error) {
	if err := doSomething(); err != nil { // shadows the return
		return err
	}
	defer func() {qqq
		// This 'err' is the named return, not the one above
		if err != nil {
			log.Println("deferred error:", err)
		}
	}()
	return nil
}

Using a distinct name for the named return makes the return value’s role explicit and avoids confusion:

func fn() (retErr error) {
	if err := doSomething(); err != nil {
		return err
	}
	defer func() {
		if retErr != nil {
			log.Println("deferred error:", retErr)
		}
	}()
	return nil
}

This check is opt-in and not enabled by default.

Named return values called 'err' are discouraged because they can lead
to confusion and subtle mistakes.

In most Go code, 'err' is used for short-lived local variables. Naming a
return parameter 'err' can induce cognitive load.

For example:

	func fn() (err error) {
		if err := doSomething(); err != nil { // shadows the return
			return err
		}
		defer func() {qqq
			// This 'err' is the named return, not the one above
			if err != nil {
				log.Println("deferred error:", err)
			}
		}()
		return nil
	}

Using a distinct name for the named return makes the return value’s
role explicit and avoids confusion:

	func fn() (retErr error) {
		if err := doSomething(); err != nil {
			return err
		}
		defer func() {
			if retErr != nil {
				log.Println("deferred error:", retErr)
			}
		}()
		return nil
	}

This check is opt-in and not enabled by default.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
@thaJeztah thaJeztah force-pushed the check_named_err_return branch from 16fd734 to 2a9f7ad Compare August 2, 2025 20:31
Comment on lines +57 to +59
Since: "2025.2",
NonDefault: true,
MergeIf: lint.MergeIfAll,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hope I did this correct; I wasn't sure what Since to add here, and assumed that NonDefault: false meant that it's an opt-in.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use "Unreleased" for checks that aren't part of an existing release yet. That gets updated on release day.

You're right about the use of NonDefault.

@thaJeztah
Copy link
Author

thaJeztah commented Aug 2, 2025

@dominikh ptal; I'm really unfamiliar with this codebase, so hope I did it correct (tests passed locally, so that's a win 😅). This was a bit of a pet-peeve of mine (err for named outputs), and I didn't know of any linter that had a check for this, so thought I'd give it a try, taking parts of https://github.com/dominikh/go-tools/tree/master/stylecheck/st1008 as an example.

I hope it's something you're willing to accept (I think I made it "opt-in"), but happy to hear your thoughts! (if not accepted, it was a fun exercise 😂)

Copy link
Owner

@dominikh dominikh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as the PR itself is concerned, this is great work. I am, however, not sure this check is suitable for Staticcheck. Style checks, even non-default ones, have to be pretty much universally true, if not official. The checks that are non-default are non-default because they--while correct--can be a bit much to worry about constantly, not because people are able to disagree with them (at least that's the goal).

So when you say

Named return values called 'err' are discouraged because they can lead to confusion and subtle mistakes.

it comes down to this: discouraged by whom, and always? Because it doesn't seem all that discouraged in Go's own code base:

rg 'func.+\).+err error\)' $(go env GOROOT)/src | wc -l
5637

if err := doSomething(); err != nil { // shadows the return
return err
}
defer func() {qqq
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qqq?

return err
}
defer func() {qqq
// This 'err' is the named return, not the one above
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand how this is a convincing or illustrative example. If the user thought that err referred to a local variable, wouldn't the whole check be redundant, since that err had to be nil to get here?

Comment on lines +57 to +59
Since: "2025.2",
NonDefault: true,
MergeIf: lint.MergeIfAll,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use "Unreleased" for checks that aren't part of an existing release yet. That gets updated on release day.

You're right about the use of NonDefault.

return nil
}

This check is opt-in and not enabled by default.`,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be part of the docstring. We will (should?) generate the correct output from the value of NonDefault.

errorType := types.Universe.Lookup("error").Type()

fnLoop:
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need the power of the IR here. Instead, I'd use an inspector.Inspector to look at the ast.FuncDecls directly.

This doesn't really matter for staticcheck (our command) because we have to build the IR form, anyway, but we may have clients that use a subset of our checks, such as only those that are fast, which excludes those that rely on the IR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants