affaan-m/ECC · error

querying user %s: %w

Error message

querying user %s: %w

What it means

Wrapped error from UserRepository.FindByID in the ECC error-handling skill. The repository calls r.db.QueryRow; when the returned error is neither nil nor sql.ErrNoRows (i.e. a real query failure), it is wrapped with the queried user id via fmt.Errorf("querying user %s: %w", id, err). The %w verb preserves the original error so callers can errors.Is / errors.As against it. The distinct sentinel ErrNotFound is reserved for the no-rows case, so this message specifically means 'something other than not-found went wrong at the database layer'.

Source

Thrown at skills/error-handling/SKILL.md:277

package domain

import "errors"

// Sentinel errors for type-checking
var (
    ErrNotFound    = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrConflict     = errors.New("conflict")
)

// Wrap errors with context — never lose the original
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
    user, err := r.db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
    }
    if err != nil {
        return nil, fmt.Errorf("querying user %s: %w", id, err)
    }
    return user, nil
}

// At the handler level, unwrap to determine response
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    user, err := h.service.GetUser(r.Context(), chi.URLParam(r, "id"))
    if err != nil {
        switch {
        case errors.Is(err, domain.ErrNotFound):
            writeError(w, http.StatusNotFound, "not_found", err.Error())
        case errors.Is(err, domain.ErrUnauthorized):
            writeError(w, http.StatusForbidden, "forbidden", "Access denied")
        default:
            slog.Error("unexpected error", "err", err)
            writeError(w, http.StatusInternalServerError, "internal_error", "An unexpected error occurred")
        }
        return

View on GitHub (pinned to 01e15490f0)

Solutions

  1. At the caller, unwrap with errors.As to classify the driver-level error and decide retry vs. fail (e.g. retry on pgconn.PgError with code 08001, surface 5xx otherwise).
  2. Check database connectivity and credentials for the current environment.
  3. Verify the users table exists and the schema matches the SELECT * projection.
  4. If the failure is context-bound, raise the caller's timeout or move long work off the request context.
  5. Log the wrapped error (err) at debug level so the original driver error is recoverable while users only see a generic message.

Example fix

// before
user, err := h.service.GetUser(ctx, id)
if err != nil {
    return err
}

// after - distinguish retriable driver errors at the handler
user, err := h.service.GetUser(ctx, id)
if err != nil {
    var pgErr *pgconn.PgError
    if errors.As(err, &pgErr) && isRetriableCode(pgErr.Code) {
        return retryLater(err)
    }
    return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate id shape and ctx deadline before the query.
func validateForQuery(ctx context.Context, id string) error {
    if id == "" {
        return errors.New("empty user id")
    }
    if _, ok := ctx.Deadline(); !ok {
        return errors.New("ctx must have a deadline for db queries")
    }
    return nil
}

Type guard

// Narrow driver-level errors to decide retry vs. surface.
func isRetriableDBError(err error) bool {
    var pgErr *pgconn.PgError
    if errors.As(err, &pgErr) {
        switch pgErr.Code {
        case "08000", "08003", "08006", "08001", "08004":
            return true
        }
    }
    var netErr net.Error
    return errors.As(err, &netErr) && (netErr.Timeout() || netErr.Temporary())
}

Try / catch

user, err := r.db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id)
if errors.Is(err, sql.ErrNoRows) {
    return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
}
if err != nil {
    if isRetriableDBError(err) {
        // back off and retry, or wrap as retriable for the caller
    }
    return nil, fmt.Errorf("querying user %s: %w", id, err)
}

Prevention

When it happens

Trigger: Calling FindByID(ctx, id) when r.db.QueryRow returns a non-nil error that is NOT sql.ErrNoRows. Concrete triggers: the database connection is broken, the query context is cancelled (ctx.Done), the SQL is rejected (syntax error, permission denied on the users table), or the driver returns a transient connection error.

Common situations: Connection pool exhausted or dropped mid-flight; context deadline exceeded because the caller set a tight timeout; the users table is missing or the role lacks SELECT; malformed id causes a parameter binding failure; running against an un-migrated database where the schema differs from the query.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/f9f0fb1a37f881d7. Report an issue: GitHub.