{"record":{"id":"4ab1406535543e11","repo":"affaan-m/ECC","slug":"user-s-w","errorCode":null,"errorMessage":"user %s: %w","messagePattern":"user (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"info","filePath":"skills/error-handling/SKILL.md","lineNumber":274,"sourceCode":"### Sentinel Errors and Error Wrapping\n\n```go\npackage 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)","sourceCodeStart":256,"sourceCodeEnd":292,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/skills/error-handling/SKILL.md#L256-L292","documentation":"A Go fmt.Errorf format string used inside UserRepository.FindByID to wrap the domain sentinel ErrNotFound with the queried id: fmt.Errorf(\"user %s: %w\", id, ErrNotFound). This preserves the sentinel (so errors.Is(err, ErrNotFound) still works at the handler) while adding the id of the user that was not found.","triggerScenarios":"FindByID is called with an id; the underlying query returns sql.ErrNoRows; the code converts that to ErrNotFound wrapped with the id. The wrapped error propagates up to the handler which unwraps via errors.Is to decide the HTTP response.","commonSituations":"Handler does not unwrap — returns 500 for a 404 situation; using %v instead of %w breaks the handler's errors.Is check; the id is logged in clear text (PII concern); inconsistent wrapping across repositories (some wrap, some return raw sql.ErrNoRows).","solutions":["Always use %w when wrapping sentinels so errors.Is keeps working at higher layers.","At the handler, branch on errors.Is(err, ErrNotFound) to return 404.","Wrap consistently across all repositories so handlers can rely on the sentinel.","If id is sensitive, redact it in the wrapped message or in logs."],"exampleFix":"// before\nreturn nil, fmt.Errorf(\"user %s: %w\", id, ErrNotFound)\n\n// after: handler-side unwrapping for the correct status\nuser, err := h.service.GetUser(ctx, id)\nif err != nil {\n    if errors.Is(err, domain.ErrNotFound) {\n        return nil, &HTTPError{Code: 404, Message: \"user not found\"}\n    }\n    return nil, fmt.Errorf(\"get user: %w\", err)\n}","handlingStrategy":"try-catch","validationCode":"null","typeGuard":"null","tryCatchPattern":"user, err := repo.FindByID(ctx, id)\nif err != nil {\n    if errors.Is(err, domain.ErrNotFound) {\n        return nil, &HTTPError{Code: 404, Message: \"user not found\"}\n    }\n    return nil, fmt.Errorf(\"get user %s: %w\", id, err)\n}","preventionTips":["Always use %w when wrapping a sentinel so errors.Is still matches.","Unwrap consistently at the handler to map to the correct status code.","Wrap across all repositories so handlers can rely on the sentinel."],"tags":["go","error-wrapping","repository","sentinel-error","fmt"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}