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")
}
returnView on GitHub (pinned to 01e15490f0)
Solutions
- 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).
- Check database connectivity and credentials for the current environment.
- Verify the users table exists and the schema matches the SELECT * projection.
- If the failure is context-bound, raise the caller's timeout or move long work off the request context.
- 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
- Always set a context deadline before issuing db queries.
- Validate id format at the API boundary before it reaches the repository.
- Use a connection pool sized for the load to avoid pool-exhaustion errors.
- Log the wrapped cause at boundaries so the original driver error is recoverable.
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.