ErrLookupBackground articles › Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them

Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them

"Record not found" errors — ActiveRecord::RecordNotFound, Prisma P2025, "Transaction not found", "File not found", and dozens of similar messages — mean a lookup by id returned nothing: the row was deleted, never existed, belongs to someone else, or is hidden by scoping or permissions. This guide explains why lookups fail across frameworks and how to fix and prevent them.

Distilled from 90 documented records across 28 repositories.

Background

The record-not-found family is produced by the data-access layer: a query by primary key (or by id plus scoping filters) matched zero rows, and the library converts that empty result into an exception or error value. Rails raises ActiveRecord::RecordNotFound and renders it as HTTP 404; Prisma surfaces P2025 ('An operation failed because it depends on one or more records that were required but not found'); Rust stores compare affected-row counts to zero; Firestore adapters throw when a getDoc read-back yields no document. The mechanism is identical everywhere — a findOne/find/get returns null and the caller decided that null is a failure, not a normal result.

What makes the family subtle is that 'not found' is almost never just 'the id is wrong'. In scoped APIs, the id may exist but be invisible: we-promise/sure looks transactions, securities, prices, syncs, and recurring transactions up through family-scoped queries, so another family's valid UUID 404s exactly like a nonexistent one, and for recurring transactions the write endpoints apply a stricter writable-by scope than the read endpoint — a record can be readable but not updatable. Phabricator loads inline comments and their container objects through policy-aware queries with the acting viewer, so a deleted or policy-hidden comment is indistinguishable from a garbage PHID. OpenProject's identityUrl lookup, sure's rejected-transfer and family-export lookups, and nautilus_trader's FOR UPDATE claim on execution intents all layer ownership or state filters on top of the plain id match.

Some members of the family are timing races rather than bad input. A row can vanish between two steps: Rocket.Chat's video-conf-changed event re-reads a call document that housekeeping already deleted; zeroclaw's cron store finds zero rows when a declarative job was removed while a run was in flight; AFFiNE's mark-as-read loses a race with a second tab consuming the same notification; anything-llm deletes the parsed-file row in a finally block, so any second call with the same fileId lands in 'File not found'. Others are deliberate one-shot semantics: openhuman's remove_source and Rocket.Chat's deleteCustomUserStatus throw on delete-of-nothing so callers can distinguish 'deleted' from 'nothing was there' — though several of those same libraries' docs recommend treating not-found-on-delete as idempotent success in practice.

The correct response is therefore library- and operation-specific. For reads, a 404 usually means refresh your source of ids: re-list from the same API or table rather than reusing cached, cross-environment, or hand-typed identifiers. For deletes and mark-as-read, many maintainers advise treating not-found as success. And where libraries differ on whether not-found is an internal integrity failure (Phabricator throws a plain Exception) or a normal user-facing 404 (sure renders {error: 'record_not_found'}), the underlying rule is the same: validate id format early, look rows up through the same scoping the operation uses, and re-fetch rather than retry with a stale id.

Common causes

What usually fixes it

Go deeper

Documented occurrences

…and 70 more across the corpus — use search.

Honest provenance: generated on 2026-08-28 from AI-assisted analysis of the linked records. See how records are made.