go-chi/chi · error · ErrResponse
article not found.
Error message
article not found.
What it means
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).
Source
Thrown at _examples/rest/main.go:510
func dbUpdateArticle(id string, article *Article) (*Article, error) {
for i, a := range articles {
if a.ID == id {
articles[i] = article
return article, nil
}
}
return nil, errors.New("article not found.")
}
func dbRemoveArticle(id string) (*Article, error) {
for i, a := range articles {
if a.ID == id {
articles = append((articles)[:i], (articles)[i+1:]...)
return a, nil
}
}
return nil, errors.New("article not found.")
}
func dbGetUser(id int64) (*User, error) {
for _, u := range users {
if u.ID == id {
return u, nil
}
}
return nil, errors.New("user not found.")
}
View on GitHub (pinned to 8b258c7bb2)
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.
Example fix
// before
func DeleteArticle(w http.ResponseWriter, r *http.Request) {
article := r.Context().Value("article").(*Article)
article, err = dbRemoveArticle(article.ID)
if err != nil {
render.Render(w, r, ErrInvalidRequest(err)) // -> 400 "article not found."
return
}
render.Render(w, r, NewArticleResponse(article))
}
// after
var ErrArticleNotFound = errors.New("article not found")
func DeleteArticle(w http.ResponseWriter, r *http.Request) {
article := r.Context().Value("article").(*Article)
removed, err := dbRemoveArticle(article.ID)
if err != nil {
if errors.Is(err, ErrArticleNotFound) {
render.Render(w, r, ErrNotFound) // -> 404
return
}
render.Render(w, r, ErrInvalidRequest(err))
return
}
render.Status(r, http.StatusOK)
render.Render(w, r, NewArticleResponse(removed))
} Defensive patterns
Strategy: validation
Validate before calling
// Before deleting, confirm the id resolves (cheap GET against the same store).
if _, err := dbGetArticle(id); err != nil {
// already absent - decide idempotent success vs 404
return http.StatusOK // or render ErrNotFound
}
_, _ = dbRemoveArticle(id) Type guard
// Distinguish 'not found' from other errors using a typed sentinel.
var ErrArticleNotFound = errors.New("article not found")
func isNotFound(err error) bool { return errors.Is(err, ErrArticleNotFound) } Try / catch
removed, err := dbRemoveArticle(id)
if err != nil {
if isNotFound(err) {
render.Render(w, r, ErrNotFound) // 404
return
}
render.Render(w, r, ErrInvalidRequest(err)) // 400
return
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of go-chi/chi@8b258c7bb2 (2026-08-04).
Data as JSON: /data/errors/172eb086dd6c2ef2.json.
Report an issue: GitHub.