{"record":{"id":"f9f0fb1a37f881d7","repo":"affaan-m/ECC","slug":"querying-user-s-w","errorCode":null,"errorMessage":"querying user %s: %w","messagePattern":"querying user (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"skills/error-handling/SKILL.md","lineNumber":277,"sourceCode":"package domain\n\nimport \"errors\"\n\n// Sentinel errors for type-checking\nvar (\n    ErrNotFound    = errors.New(\"not found\")\n    ErrUnauthorized = errors.New(\"unauthorized\")\n    ErrConflict     = errors.New(\"conflict\")\n)\n\n// Wrap errors with context — never lose the original\nfunc (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {\n    user, err := r.db.QueryRow(ctx, \"SELECT * FROM users WHERE id = $1\", id)\n    if errors.Is(err, sql.ErrNoRows) {\n        return nil, fmt.Errorf(\"user %s: %w\", id, ErrNotFound)\n    }\n    if err != nil {\n        return nil, fmt.Errorf(\"querying user %s: %w\", id, err)\n    }\n    return user, nil\n}\n\n// At the handler level, unwrap to determine response\nfunc (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {\n    user, err := h.service.GetUser(r.Context(), chi.URLParam(r, \"id\"))\n    if err != nil {\n        switch {\n        case errors.Is(err, domain.ErrNotFound):\n            writeError(w, http.StatusNotFound, \"not_found\", err.Error())\n        case errors.Is(err, domain.ErrUnauthorized):\n            writeError(w, http.StatusForbidden, \"forbidden\", \"Access denied\")\n        default:\n            slog.Error(\"unexpected error\", \"err\", err)\n            writeError(w, http.StatusInternalServerError, \"internal_error\", \"An unexpected error occurred\")\n        }\n        return","sourceCodeStart":259,"sourceCodeEnd":295,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/skills/error-handling/SKILL.md#L259-L295","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nuser, err := h.service.GetUser(ctx, id)\nif err != nil {\n    return err\n}\n\n// after - distinguish retriable driver errors at the handler\nuser, err := h.service.GetUser(ctx, id)\nif err != nil {\n    var pgErr *pgconn.PgError\n    if errors.As(err, &pgErr) && isRetriableCode(pgErr.Code) {\n        return retryLater(err)\n    }\n    return err\n}","handlingStrategy":"try-catch","validationCode":"// Validate id shape and ctx deadline before the query.\nfunc validateForQuery(ctx context.Context, id string) error {\n    if id == \"\" {\n        return errors.New(\"empty user id\")\n    }\n    if _, ok := ctx.Deadline(); !ok {\n        return errors.New(\"ctx must have a deadline for db queries\")\n    }\n    return nil\n}","typeGuard":"// Narrow driver-level errors to decide retry vs. surface.\nfunc isRetriableDBError(err error) bool {\n    var pgErr *pgconn.PgError\n    if errors.As(err, &pgErr) {\n        switch pgErr.Code {\n        case \"08000\", \"08003\", \"08006\", \"08001\", \"08004\":\n            return true\n        }\n    }\n    var netErr net.Error\n    return errors.As(err, &netErr) && (netErr.Timeout() || netErr.Temporary())\n}","tryCatchPattern":"user, err := r.db.QueryRow(ctx, \"SELECT * FROM users WHERE id = $1\", id)\nif errors.Is(err, sql.ErrNoRows) {\n    return nil, fmt.Errorf(\"user %s: %w\", id, ErrNotFound)\n}\nif err != nil {\n    if isRetriableDBError(err) {\n        // back off and retry, or wrap as retriable for the caller\n    }\n    return nil, fmt.Errorf(\"querying user %s: %w\", id, err)\n}","preventionTips":["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."],"tags":["go","database","error-wrapping","repository-pattern"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}