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

Drop multierr module and rely on the standard lib #4299

Merged
merged 1 commit into from
May 22, 2024

Conversation

twz123
Copy link
Member

@twz123 twz123 commented Apr 16, 2024

Description

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update

How Has This Been Tested?

  • Manual test
  • Auto test added

Checklist:

  • My code follows the style guidelines of this project
  • My commit messages are signed-off
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules
  • I have checked my code and corrected any misspellings

@twz123 twz123 added the chore label Apr 16, 2024
@twz123 twz123 marked this pull request as ready for review April 16, 2024 16:44
@twz123 twz123 requested a review from a team as a code owner April 16, 2024 16:44
@twz123 twz123 requested review from kke and jnummelin April 16, 2024 16:44
if err == nil {
err = closeErr
} else if closeErr != nil {
err = fmt.Errorf("%w; %w", err, closeErr)
Copy link
Contributor

@kke kke Apr 17, 2024

Choose a reason for hiding this comment

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

There's a ; there.

I suppose defer func() { err = errors.Join(err, in.Close()) } would do the same except if the %w: %w formatting is important.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not exactly the same. This is touching some of the things I preferred in the multierr implementation.

  1. multierr used "; " to join error messages, while errors.Join(...) uses "\n", which has puzzled me since its inception. So yes, the semicolon is intentional, as this is two equitable errors, not one that's been caused by another one, where a colon would be more appropriate.
  2. errors.Join(...) wraps the error in any case, even if there's only one, so errors.Join(nil, err) != err, whereas multierr did not wrap a single error: multierr.Combine(nil, err) == err. Same for multierr.Append(...). I wonder why upstream decided to do it that way. Maybe because of 3.
  3. I wonder why there isn't a public error type for "a slice of errors", which is basically what errors.Join(...) represents. To me, there's a fundamental difference between "multiple errors occurred" (which is "a slice of errors" / errors.Join(...)) and "an error occurred that has multiple causes" (which is fmt.Errorf(...) with multiple %w placeholders, or some other custom error type that implements Unwrap() []error). The way the Go standard library does it makes these cases indistinguishable.

Those things bother me in the way that things bother people even when they know they really shouldn't care that much 🙈

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor

Choose a reason for hiding this comment

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

Go docs say:

When adding additional context to an error, either with fmt.Errorf or by implementing a custom type, you need to decide whether the new error should wrap the original. There is no single answer to this question; it depends on the context in which the new error is created. Wrap an error to expose it to callers. Do not wrap an error when doing so would expose implementation details.

As one example, imagine a Parse function which reads a complex data structure from an io.Reader. If an error occurs, we wish to report the line and column number at which it occurred. If the error occurs while reading from the io.Reader, we will want to wrap that error to allow inspection of the underlying problem. Since the caller provided the io.Reader to the function, it makes sense to expose the error produced by it.

In contrast, a function which makes several calls to a database probably should not return an error which unwraps to the result of one of those calls. If the database used by the function is an implementation detail, then exposing these errors is a violation of abstraction. For example, if the LookupUser function of your package pkg uses Go’s database/sql package, then it may encounter a sql.ErrNoRows error. If you return that error with fmt.Errorf("accessing DB: %v", err) then a caller cannot look inside to find the sql.ErrNoRows. But if the function instead returns fmt.Errorf("accessing DB: %w", err), then a caller could reasonably write

err := pkg.LookupUser(...)
if errors.Is(err, sql.ErrNoRows) …

At that point, the function must always return sql.ErrNoRows if you don’t want to break your clients, even if you switch to a different database package. In other words, wrapping an error makes that error part of your API. If you don’t want to commit to supporting that error as part of your API in the future, you shouldn’t wrap the error.

It’s important to remember that whether you wrap or not, the error text will be the same. A person trying to understand the error will have the same information either way; the choice to wrap is about whether to give programs additional information so they can make more informed decisions, or to withhold that information to preserve an abstraction layer.

So, in that light, %v is sometimes better than %w and errors.Join is not a replacement for adding context to error messages 🤔

Copy link
Contributor

The PR is marked as stale since no activity has been recorded in 30 days

@github-actions github-actions bot added the Stale label May 18, 2024
@kke kke removed the Stale label May 20, 2024
@twz123 twz123 merged commit a3bc75d into k0sproject:main May 22, 2024
75 checks passed
@twz123 twz123 deleted the drop-multierr branch May 22, 2024 07:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants