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

  1. Verify the article still exists (GET /articles/{id}) before deleting, or treat a 400/404 on DELETE as 'already gone' and accept it.
  2. Make deletion idempotent: in dbRemoveArticle return a typed sentinel and have the handler map it to 404 (or 204) rather than 400.
  3. Ensure the client is not double-submitting the DELETE (add an idempotency key or debounce the button).
  4. 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

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.