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

wiki: CodeReviewComments: Error documentation #49254

Open
7 tasks
sfllaw opened this issue Nov 1, 2021 · 8 comments
Open
7 tasks

wiki: CodeReviewComments: Error documentation #49254

sfllaw opened this issue Nov 1, 2021 · 8 comments
Labels
Documentation NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made.
Milestone

Comments

@sfllaw
Copy link
Contributor

sfllaw commented Nov 1, 2021

Problem

Go Code Review Comments contains various bits of advice around errors:

However, there isn’t any good advice on how to deal with:

  • defining sentinel errors and checking them with errors.Is,
  • defining custom error types and checking them with errors.As,
  • wrapping errors with additional context using fmt.Errorf.

In addition, the Handle Errors redirects readers to some obsolete advice in Effective Go: Errors, which is highlighted by #45535. Since Effective Go will likely be frozen in the near future, see #28782, we should update the link with something more modern.

This work is motivated by #49172 (comment) where we realized that we need to discourage dependencies from doing error string comparisons, like .Error() == "...", so that corrections to error strings won’t break downstream programs.

Proposal

The following items are proposed changes to consider, in no particular order:

  • Change Handle Errors to point to Working with Errors in Go 1.13 - go.dev.
  • Be more specific in Handle Errors to mention checking with errors.Is and errors.As, with examples for common idioms.
  • Change In-Band Errors so it describes error wrapping with fmt.Errorf, since the current example encourages consumers to check the Error string for a substring.
  • Add Sentinel Errors to discuss common sentinels like io.EOF and sql.ErrNoRows, declaring custom sentinels with errors.New, and recommend errors.Is for checking errors. I acknowledge that many of these errors are actually == equal, but that is subtly brittle for new Go programmers.
  • Add Error Types that explains how to create a custom error type, referencing Error handling in Upspin, and recommend using errors.As instead of a type assertion.
  • Add Wrapping Errors to discuss how to use fmt.Errorf with %w to wrap an error with additional context, instead of returning the naked error.
  • Additional errors proposed in the comments below.

Implementation

Drafts of each section will be included in the comments below. Please feel free to suggest changes to any of the proposed sections. Please also vote 👍 or 👎 to indicate whether we should include a given section.

@sfllaw
Copy link
Contributor Author

sfllaw commented Nov 1, 2021

Handle Errors

Do not discard errors using _ variables. If a function returns an error, check it to make sure the function succeeded. Handle the error or return it, both don’t do both. In truly exceptional situations, panic.

var db *sql.DB

func QueryUsername(ctx context.Context, query string) (string, error) {
	var username string
	err := db.QueryRowContext(ctx, `SELECT username FROM users WHERE username ILIKE ?`, query).Scan(&s)
	if errors.Is(err, sql.ErrNoRows) {
		// Handle the error.
		return "", nil
	}
	if err != nil {
		// Wrap the error with additional context.
		// Use %v to avoid importing database/sql errors into your API.
		return "", fmt.Errorf("query username: %v", id, err)
	}
	return username, nil
}

Avoid handling errors by == equality or the error string returned by the error interface, since these fail when errors are wrapped. Most errors can be identified by using errors.Is or errors.As.

Read more about examining errors with Is and As.

Notes

The example has been altered from database/sql.DB.QueryRowContext to emphasize error handling.

In the actual example code, a switch statement is used. This seems to be a matter of preference, but there is some subtlety with breaking out of a switch statement which I wanted to avoid. Thoughts?

In #49356 (comment), @dottedmag points out that there are some opaque errors where the only option is to use string comparison. Should we recommend something like strings.Contains in the event that you really know what you’re doing? Or will that confuse beginners?

@sfllaw
Copy link
Contributor Author

sfllaw commented Nov 1, 2021

In-Band Errors

And encourages more robust and readable code:

value, ok := Lookup(key)
if !ok {
	return fmt.Errorf("%w for %q, ErrNoValue, key)
}
return Parse(value)

Notes

Actually, I’m no longer sure that adding the error sentinel to fmt.Errorf makes things any clearer. I can’t think of a way to revise this advice without confusing things, even though ErrNoValue does seem like it should be an error sentinel. Feedback from the comment section would be greatly appreciated.

@sfllaw
Copy link
Contributor Author

sfllaw commented Nov 1, 2021

Sentinel Errors

Export sentinel errors from your package when your users need to check for an identifiable error condition:

var ErrNotFound = errors.New("not found")

// FetchItem returns the named item.
//
// If no item with the name exists,
// FetchItem returns an error wrapping ErrNotFound.
func FetchItem(name string) (*Item, error) {
	if itemNotFound(name) {
		return nil, fmt.Errorf("fetch item %q: %w", name, ErrNotFound)
	}
	// ...
}

Callers can then handle this condition when the item is not found:

item, err := FetchItem(name)
if errors.Is(err, ErrNotFound) {
	item = defaultItem
} else if err != nil {
	return fmt.Errorf("additional context: %w", err)
}

You should define your own sentinel errors unless you are willing to support third-party errors as part of your public API. This is true even for the sentinel errors defined in the standard library.

Note that standard interfaces like io.Reader are defined to return the standard io.EOF sentinel, not a wrapped version:

// EmptyReader represents an empty reader.
// In practice, use strings.NewReader("") instead.
type EmptyReader struct{}

// Read implements the io.Reader interface.
// For an EmptyReader, Read always returns 0, io.EOF.
func (r EmptyReader) Read(p []byte) (n int, err error) {
	return 0, io.EOF // Read must never wrap io.EOF.
}

Read more about errors and package APIs.

Notes

The first example is also the example from errors and package APIs. I have added an example for how the caller would handle a sentinel error.

For the third example, I tried to generate something minimal to illustrate why we cannot wrap io.EOF when implementing io.Reader, but it seems rather contrived. Perhaps someone can suggest something better?

@sfllaw
Copy link
Contributor Author

sfllaw commented Nov 1, 2021

Error Types

Export a custom error type from your package when your users need to check for an identifiable error that includes details describing the error:

type QueryError struct {
	Query  string
	Line   int
	Column int
	Err    error
}

// Error implements the error interface.
func (e *QueryError) Error() string {
	return fmt.Sprintf("%d:%d: %s: %v", e.Line, e.Column, e.Query, e.err)
}

// Unwrap is called by errors.Unwrap to unpack the wrapped err.
func (e *QueryError) Unwrap() error {
	return e.err
}

Callers can then handle the bad query by unwrapping the error:

data, err := Query(q)
if err != nil {
	var qerr *QueryError
	if errors.As(err, &qerr) {
		// Handle the query error by updating the UI
		form.SetQuery(qerr.Query)
		form.SetPos(qerr.Line, qerr.Column)
		form.SetTooltip(qerr.Err.Error())
	} else {
		return fmt.Errorf("query failed: %v", err)
	}
}

You should define your own error types unless you are willing to support third-party errors as part of your public API. This is true even for the error types defined in the standard library.

Read more about errors in Go 1.13. For an example of a richer error type, see Error handling in Upspin.

Notes

The example QueryError is a richer version of the one found in https://go.dev/blog/go1.13-errors to distinguish it from how a sentinel error would normally be used. Is this too complicated?

I didn’t mention making the Error a pointer receiver to prevent equality checks, should I?

I also mention upspin.io/errors even though that package doesn’t implement Unwrap. Is that going to confuse people?

@thanm thanm added the NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. label Nov 1, 2021
@thanm thanm added this to the Backlog milestone Nov 1, 2021
@ianlancetaylor
Copy link
Contributor

CC @neild @jba

@sfllaw
Copy link
Contributor Author

sfllaw commented Nov 3, 2021

Wrapping Errors

Use fmt.Errorf to judiciously wrap errors so that users can locate the code that generated the error, instead of returning an error directly. Avoid breaking error strings in your code so that other developers can search for them using tools like find or grep.

package gzip

func Decompress(name string) error {
	f, err := os.Open(filename)
	if err != nil {
		// Return an error which unwraps to err.
		return fmt.Errorf("gzip: decompress %v: %w", name, err)
	}
}

When formatting errors, carefully consider whether to use %w which will wrap an error, or %v which won’t. Wrapping an error outside your package ties your public API to the underlying implementation, which may lead to backwards-incompatible changes if you switch out a third-party library.

Notes

I’m wondering how useful the advice in this section will be, given the other sections I’ve proposed. Comments?

@jba
Copy link
Contributor

jba commented Nov 8, 2021

@sfllaw, thanks for these suggestions. I'm going to try to find time this week to give them a careful read.

@thexpand
Copy link

thexpand commented Mar 29, 2023

I see this is still open. Is it still relevant? Should one resolve to the current description (https://github.com/golang/go/wiki/CodeReviewComments) or should we consider this discussion here to have more viable solutions?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Documentation NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made.
Projects
None yet
Development

No branches or pull requests

6 participants