{"id":"172eb086dd6c2ef2","repo":"go-chi/chi","slug":"article-not-found","errorCode":null,"errorMessage":"article not found.","messagePattern":"article not found\\.","errorType":"exception","errorClass":"ErrResponse","httpStatus":400,"severity":"error","filePath":"_examples/rest/main.go","lineNumber":510,"sourceCode":"\nfunc dbUpdateArticle(id string, article *Article) (*Article, error) {\n\tfor i, a := range articles {\n\t\tif a.ID == id {\n\t\t\tarticles[i] = article\n\t\t\treturn article, nil\n\t\t}\n\t}\n\treturn nil, errors.New(\"article not found.\")\n}\n\nfunc dbRemoveArticle(id string) (*Article, error) {\n\tfor i, a := range articles {\n\t\tif a.ID == id {\n\t\t\tarticles = append((articles)[:i], (articles)[i+1:]...)\n\t\t\treturn a, nil\n\t\t}\n\t}\n\treturn nil, errors.New(\"article not found.\")\n}\n\nfunc dbGetUser(id int64) (*User, error) {\n\tfor _, u := range users {\n\t\tif u.ID == id {\n\t\t\treturn u, nil\n\t\t}\n\t}\n\treturn nil, errors.New(\"user not found.\")\n}\n","sourceCodeStart":492,"sourceCodeEnd":521,"githubUrl":"https://github.com/go-chi/chi/blob/8b258c7bb28f97a5f2a856ff7ef962578fec9215/_examples/rest/main.go#L492-L521","documentation":"Returned by dbRemoveArticle when no element of the in-memory articles slice matches the supplied id. The DeleteArticle handler (main.go:209) calls dbRemoveArticle(article.ID) and, on err != nil, renders it as a 400 'Invalid request.' ErrResponse with this text. Note the status code mismatch: the lookup genuinely failed but the handler reuses ErrInvalidRequest rather than ErrNotFound, so clients see 400 instead of 404. The same string is also produced by dbGetArticle, dbGetArticleBySlug, and dbUpdateArticle for the same reason (absent id).","triggerScenarios":"DELETE /articles/{articleID} where articleID is not in the fixture set (e.g. DELETE /articles/999). Also reachable indirectly: the ArticleCtx middleware already 404s unknown ids, so in practice this DeleteArticle branch fires only when an article was removed by a concurrent request between the ArticleCtx load and the delete, or when the id was mutated inside the handler chain.","commonSituations":"Calling DELETE twice on the same resource (the second call finds it gone); a race where two requests delete the same article; clients passing a stale id cached from an earlier session; tests that delete fixture id '1' and then delete again expecting idempotency.","solutions":["Verify the article still exists (GET /articles/{id}) before deleting, or treat a 400/404 on DELETE as 'already gone' and accept it.","Make deletion idempotent: in dbRemoveArticle return a typed sentinel and have the handler map it to 404 (or 204) rather than 400.","Ensure the client is not double-submitting the DELETE (add an idempotency key or debounce the button).","If this is a concurrency race, serialize mutations per id with a lock or a single-writer queue."],"exampleFix":"// before\nfunc DeleteArticle(w http.ResponseWriter, r *http.Request) {\n    article := r.Context().Value(\"article\").(*Article)\n    article, err = dbRemoveArticle(article.ID)\n    if err != nil {\n        render.Render(w, r, ErrInvalidRequest(err)) // -> 400 \"article not found.\"\n        return\n    }\n    render.Render(w, r, NewArticleResponse(article))\n}\n\n// after\nvar ErrArticleNotFound = errors.New(\"article not found\")\n\nfunc DeleteArticle(w http.ResponseWriter, r *http.Request) {\n    article := r.Context().Value(\"article\").(*Article)\n    removed, err := dbRemoveArticle(article.ID)\n    if err != nil {\n        if errors.Is(err, ErrArticleNotFound) {\n            render.Render(w, r, ErrNotFound) // -> 404\n            return\n        }\n        render.Render(w, r, ErrInvalidRequest(err))\n        return\n    }\n    render.Status(r, http.StatusOK)\n    render.Render(w, r, NewArticleResponse(removed))\n}","handlingStrategy":"validation","validationCode":"// Before deleting, confirm the id resolves (cheap GET against the same store).\nif _, err := dbGetArticle(id); err != nil {\n    // already absent - decide idempotent success vs 404\n    return http.StatusOK // or render ErrNotFound\n}\n_, _ = dbRemoveArticle(id)","typeGuard":"// Distinguish 'not found' from other errors using a typed sentinel.\nvar ErrArticleNotFound = errors.New(\"article not found\")\n\nfunc isNotFound(err error) bool { return errors.Is(err, ErrArticleNotFound) }","tryCatchPattern":"removed, err := dbRemoveArticle(id)\nif err != nil {\n    if isNotFound(err) {\n        render.Render(w, r, ErrNotFound) // 404\n        return\n    }\n    render.Render(w, r, ErrInvalidRequest(err)) // 400\n    return\n}","preventionTips":["Use a typed sentinel (ErrArticleNotFound) and errors.Is instead of string matching.","Make DELETE idempotent: returning 404 (or 204) on a missing resource avoids client confusion.","Add an idempotency key so retried deletes do not race or double-count.","Serialize per-id mutations if concurrent deletes are possible."],"tags":["rest","crud","delete","not-found","status-code-mismatch","go"],"analyzedSha":"8b258c7bb28f97a5f2a856ff7ef962578fec9215","analyzedAt":"2026-08-04T21:43:11.924Z","schemaVersion":2}