Files
mangaBookmark/.agents/skills/golang-error-handling/references/error-wrapping.md
T
2026-07-24 16:23:24 +07:00

3.0 KiB

Error Wrapping and Inspection

Error Wrapping with %w

Wrapping preserves the original error in a chain that callers can inspect with errors.Is and errors.As. Errors SHOULD be wrapped at each layer to build a readable chain.

// ✓ Good — wraps with context, preserves the chain
func (s *UserService) GetUser(id string) (*User, error) {
    user, err := s.repo.FindByID(id)
    if err != nil {
        return nil, fmt.Errorf("getting user %s: %w", id, err)
    }
    return user, nil
}

%w vs %v: controlling exposure

Use %w within your module to preserve the error chain. Use %v at public API / system boundaries to prevent callers from depending on internal error types.

// Internal layer — wrap to preserve chain
func (r *repo) fetch(id string) error {
    return fmt.Errorf("querying database: %w", err)
}

// Public API boundary — break chain to hide internals
func (s *PublicService) GetItem(id string) error {
    err := s.repo.fetch(id)
    if err != nil {
        return fmt.Errorf("item unavailable: %v", err) // %v — callers cannot unwrap
    }
    return nil
}

Inspecting Errors: errors.Is and errors.As

errors.Is — match against a sentinel value

// ✗ Bad — direct comparison breaks on wrapped errors
if err == sql.ErrNoRows {

// ✓ Good — traverses the entire error chain
if errors.Is(err, sql.ErrNoRows) {
    return nil, ErrNotFound
}

errors.As / errors.AsType — extract a typed error from the chain

// ✗ Bad — type assertion breaks on wrapped errors
if ve, ok := err.(*ValidationError); ok {

// ✓ Good — traverses the entire error chain
var ve *ValidationError
if errors.As(err, &ve) {
    log.Printf("validation failed on field %s: %s", ve.Field, ve.Msg)
}

// ✓ Better (Go 1.26+) — same behavior, simpler syntax
if ve, ok := errors.AsType[*ValidationError](err); ok {
    log.Printf("validation failed on field %s: %s", ve.Field, ve.Msg)
}

Combining Errors with errors.Join

errors.Join (Go 1.20+) combines multiple independent errors into one. The combined error works with errors.Is and errors.As — each inner error is inspectable.

Use case: validating multiple fields

func validateUser(u User) error {
    var errs []error

    if u.Name == "" {
        errs = append(errs, errors.New("name is required"))
    }
    if u.Email == "" {
        errs = append(errs, errors.New("email is required"))
    }

    return errors.Join(errs...) // returns nil if errs is empty
}

Use case: parallel operations with independent failures

func closeAll(closers ...io.Closer) error {
    var errs []error
    for _, c := range closers {
        if err := c.Close(); err != nil {
            errs = append(errs, err)
        }
    }
    return errors.Join(errs...)
}

errors.Is works through joined errors

err := errors.Join(ErrNotFound, ErrUnauthorized)

errors.Is(err, ErrNotFound)    // true
errors.Is(err, ErrUnauthorized) // true