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

Handle errors once #179

Merged
merged 10 commits into from
Apr 13, 2023
Merged

Handle errors once #179

merged 10 commits into from
Apr 13, 2023

Conversation

abhinav
Copy link
Collaborator

@abhinav abhinav commented Apr 10, 2023

Adds new guidance on how handling errors once
rather than double handling, e.g. log-and-return.

Resolves #65
Preview

Adds new guidance on how handling errors once
rather than double handling, e.g. log-and-return.

Resolves uber-go#65
@alxn alxn self-assigned this Apr 10, 2023
@alxn
Copy link
Member

alxn commented Apr 10, 2023

The other note I like, by wrapping errors you are able to specifically test you are hitting that wrapped case. If you have a module that just passes through errors, future changes may cause those paths to change, and you to lose test coverage.

Copy link
Member

@mway mway left a comment

Choose a reason for hiding this comment

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

Looks good so far. I'm split on whether it's worth discussing error boundaries (at a high level), maybe through examples (specifically thinking of your degradation examples).

The general idea being that some confusion around error handling may stem from not really understanding what should really "care" about an error (i.e., what should that error's terminal handler be), and examples that help folks think about how to model that (through the lens of error boundaries/SOC) may help as well.

Thoughts?

@abhinav abhinav marked this pull request as ready for review April 10, 2023 19:50
@abhinav
Copy link
Collaborator Author

abhinav commented Apr 10, 2023

Could you clarify what you're imagining here, @mway?
I'm having trouble coming up with how to communicate the idea of error boundaries succinctly.
"Handle errors at abstraction boundaries" or variations of it seem a bit too high-level.

Copy link
Contributor

@sywhang sywhang left a comment

Choose a reason for hiding this comment

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

Overall I like it. Though it may get a little blurry in terms of what errors are ok to handle and what aren't in the Match the error and degrade gracefully section without some kind of guidance on error boundaries as @mway suggested.

For instance suppose someone has code similar to the example, except getUser is an external pkg:

u, err := someClient.getUser(id)

Then does one match for canonical error types defined by someClient and nothing else, like:

if errors.Is(err, someClient.ErrUserInactive) {
   // ...
}

, or does one check for any kind of commonly-occurring errors returned from someClient.getUser regardless of whether the error is wrapped or not

if errors.Is(err, fs.PathError) {
   // ...
}

@abhinav
Copy link
Collaborator Author

abhinav commented Apr 10, 2023

does one match for canonical error types defined by someClient and nothing else, [..]
or does one check for any kind of commonly-occurring errors returned from someClient.getUser regardless of whether the error is wrapped or not

Okay, yeah, this is where the confusion might be coming from.
I assumed it was obvious that you would check for whatever errors someClient.GetUser is documented as returning that you want to have special behaviors for. You will not check for fs.FooError if GetUser doesn't say that's a thing it can return, even if it uses that internally and possibly wraps it.

I'll see if there's a way to expand the match errors section.

Reorders the list of ways an error can be handled,
going from most specific to least specific,
and makes the examples more realistic for those cases.
@abhinav
Copy link
Collaborator Author

abhinav commented Apr 12, 2023

@mway @sywhang Updated to try to clarify error matching more.

I also want to clarify my intent here to help future discussion.
This is not intended to be a comprehensive list of different ways of handling errors.
It's a statement that when you handle errors (of which there are many ways, here are a few), you should do it only once.

If you think that that intent is getting lost because we enumerate some ways of handling errors, I'm happy to pare down the entry and drop those extraneous details.

@@ -0,0 +1,112 @@
# Handle Errors Once

When a function receives an error from a caller,
Copy link
Member

Choose a reason for hiding this comment

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

how does a function receive an error from a caller or am I being stupid here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

🤦 oops. yeah, callee.

Comment on lines 19 to 22
Regardless of how the error is handled, generally,
a function should handle an error only once.
It should not, for example, log the error and then return it again
because its callers will likely handle the error too.
Copy link
Member

Choose a reason for hiding this comment

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

oh, by handle you mean "take an action", where action 1 is log, and action 2 is return.

or you mean "a function should touch a received error only once."

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I meant "take an action". Let me reword a tad bit.

prashantv added a commit to prashantv/zap that referenced this pull request Apr 12, 2023
Related to uber-go/guide#179

Callsites that receive an error should either log, or return an error.

However, if the callsite has additioanl context, the simplest option is
to add it to the error, but it's then flattened into a string, losing
the benefit of structured logging. This often results in callsites
logging with additional fields, and returning an error that is likely
to be logged again.

`WrapError` provides a way for callsites to return an error that
includes fields to be logged, which will be added to an `errorFields`
key.
Copy link
Contributor

@prashantv prashantv left a comment

Choose a reason for hiding this comment

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

+1 on the guidance

Comment on lines +42 to +43
log.Printf("Could not get user %q: %v", id, err)
return err
Copy link
Contributor

Choose a reason for hiding this comment

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

(tangentially related)

I often see this with structured logging when you want id as a separate field, but returning an error forces you to flatten the id into the error string, or you have to add an additional return of what fields to log (making it much more complex)

I've been meaning to solve it for a while, and think it can be solved relatively simply,
uber-go/zap#1271


These include, but not are limited to:

- if the function contract defines specific errors,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: function can refer to the caller or callee, what do you think of explicitly using those terms throughout? "if the callee function defines specific errors"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oh good call. will change.

src/error-once.md Outdated Show resolved Hide resolved
prashantv added a commit to prashantv/zap that referenced this pull request Apr 12, 2023
Related to uber-go/guide#179

Callsites that receive an error should either log, or return an error.

However, if the callsite has additioanl context, the simplest option is
to add it to the error, but it's then flattened into a string, losing
the benefit of structured logging. This often results in callsites
logging with additional fields, and returning an error that is likely
to be logged again.

`WrapError` provides a way for callsites to return an error that
includes fields to be logged, which will be added to an `errorFields`
key.
prashantv added a commit to prashantv/zap that referenced this pull request Apr 12, 2023
Related to uber-go/guide#179

Callsites that receive an error should either log, or return an error.

However, if the callsite has additioanl context, the simplest option is
to add it to the error, but it's then flattened into a string, losing
the benefit of structured logging. This often results in callsites
logging with additional fields, and returning an error that is likely
to be logged again.

`WrapError` provides a way for callsites to return an error that
includes fields to be logged, which will be added to an `errorFields`
key.
prashantv added a commit to prashantv/zap that referenced this pull request Apr 12, 2023
Related to uber-go/guide#179

Callsites that receive an error should either log, or return an error.

However, if the callsite has additioanl context, the simplest option is
to add it to the error, but it's then flattened into a string, losing
the benefit of structured logging. This often results in callsites
logging with additional fields, and returning an error that is likely
to be logged again.

`WrapError` provides a way for callsites to return an error that
includes fields to be logged, which will be added to an `errorFields`
key.
prashantv added a commit to prashantv/zap that referenced this pull request Apr 12, 2023
Related to uber-go/guide#179

Callsites that receive an error should either log, or return an error.

However, if the callsite has additioanl context, the simplest option is
to add it to the error, but it's then flattened into a string, losing
the benefit of structured logging. This often results in callsites
logging with additional fields, and returning an error that is likely
to be logged again.

`WrapError` provides a way for callsites to return an error that
includes fields to be logged, which will be added to an `errorFields`
key.
prashantv added a commit to prashantv/zap that referenced this pull request Apr 12, 2023
Related to uber-go/guide#179

Callsites that receive an error should either log, or return an error.

However, if the callsite has additioanl context, the simplest option is
to add it to the error, but it's then flattened into a string, losing
the benefit of structured logging. This often results in callsites
logging with additional fields, and returning an error that is likely
to be logged again.

`WrapError` provides a way for callsites to return an error that
includes fields to be logged, which will be added to an `errorFields`
key.
Instead of "the function" everywhere, clarify caller/callee
relationships.
Copy link
Member

@mway mway left a comment

Choose a reason for hiding this comment

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

Nice, thanks @abhinav! LGTM modulo a couple quick suggestions.

src/error-once.md Outdated Show resolved Hide resolved
src/error-once.md Outdated Show resolved Hide resolved
abhinav and others added 3 commits April 13, 2023 09:55
Co-authored-by: Matt Way <mway@users.noreply.github.com>
Co-authored-by: Matt Way <mway@users.noreply.github.com>
@abhinav
Copy link
Collaborator Author

abhinav commented Apr 13, 2023

Thanks for the reviews, folks. Will add a changelog entry before merging.

@abhinav abhinav merged commit 42f94b6 into uber-go:master Apr 13, 2023
1 check passed
@abhinav abhinav deleted the error-once branch April 13, 2023 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Guideline: Don't Log and Return Errors
5 participants