186

errors.Is keeps returning false even though I wrapped it with %w Help

My storage package has var ErrNotFound = errors.New("not found"). The repo layer returns fmt.Errorf("loading user %d: %w", id, ErrNotFound) and the handler checks errors.Is(err, storage.ErrNotFound) to decide 404 vs 500. It works on the direct path but returns false whenever the call goes through a small retry helper I wrote. Go 1.22, no build tags, nothing clever.

8 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @committee_kim · 2mo ago · 3 replies

    Look at the retry helper. Nine times out of ten it does something like fmt.Errorf("after %d attempts: %s", n, err) or errors.New(err.Error()), and %s flattens the error into a string. The chain is gone at that point, so Is has nothing to walk. Change that one verb to %w and it will start working.

    224
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @printbed_gremlin · 2mo ago

      Yep. fmt.Errorf("giving up after %d attempts: %s", attempts, lastErr). I have been staring at the repo layer for two days.

      79
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @easy_pace_ellis · 2mo ago

      There's a vet check for this now, go vet flags a %s on an error in a Errorf that also wraps. Worth wiring into CI so nobody reintroduces it in six months.

      44
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @parquet_pile · 2mo ago · 2 replies

    If your helper collects every attempt's error rather than keeping the last one, %w alone won't save you either. Multiple wrapping needs errors.Join(errs...) or a custom type with Unwrap() []error. Since 1.20 errors.Is walks a tree, not a chain, but only if you expose it as a tree.

    118
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @blue_team_bex · 2mo ago

      And if you write that custom type, put Unwrap on the pointer receiver and return *MyErr consistently. I once had a type with a value receiver returned as a value in one place and a pointer in another, and Is matched in half the codebase.

      37
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @quietmargin · 2mo ago

    Sanity check the sentinel itself is a package-level var and not built inside a function. errors.New returns a fresh pointer every call, so if any code path does return errors.New("not found") instead of returning the shared value, comparison fails and the wrapping looks broken.

    63
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @flashcard_fen · 2mo ago · 2 replies

    errors.Is only checks one level. You have to loop with errors.Unwrap yourself if it's nested more than once.

    8
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @carbon_steel_kit · 2mo ago

      It doesn't, it unwraps repeatedly until it finds a match or hits nil. The whole point of Is over == is that it goes all the way down.

      6
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report