ErrLookup › Background 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
- Stale or cached id. The record existed when the id was captured but was deleted, purged by retention, or renumbered before use — deleted comments in Phabricator, pruned messages in Rocket.Chat, cleaned-up exports in sure, tracker rows removed by a dedup pass. Re-listing immediately before the operation resolves most of these.
- Scoping and ownership filters hiding an existing row. The lookup is scoped to a family, owner, workspace, or acting viewer, so a valid id from another tenant 404s identically to a missing one — sure's family-scoped endpoints, Phabricator's policy-filtered comment loads, anything-llm's userId filter. The row exists but is not yours.
- Malformed id (not a valid UUID/pattern). Several APIs guard format first: sure rejects non-UUID ids before the query, and openhuman/sure distinguish 'DELETE matched nothing' from SQL failure. Slugs, integers, quoted or URL-encoded UUIDs, and journal ids passed where row ids are expected all fail here.
- Wrong kind or wrong environment of id. An id from a parallel concept: an accepted transfer id queried on /rejected_transfers, a security id where a price id is expected, a transaction_journal_id passed as transactions.id in Firefly III, or a dev-environment id used against production. Ids are only valid within their own collection.
- Race between check, delete, and use. Two flows touch the same row: a call ends while its changed-event dispatch re-reads the document (Rocket.Chat), a job is deleted mid-run (zeroclaw), two tabs consume one notification (AFFiNE), a queued job and a manual call both process the same parsed file. The row disappears between two internal steps.
- One-shot or mutating-lookup semantics. Some lookups delete or transition the row as a side effect: anything-llm's moveToDocumentsAndEmbed removes the parsed-file row in a finally block, so a retry always fails. Reading the operation's contract — not just the signature — prevents guaranteed failures.
- Contradicting query constraints. Phabricator's worker tools require explicit --id values to survive all other filters, so '--id 42 --archived' fails when task 42 is active. An id plus a status, class, or type constraint that excludes it produces not-found even though the row exists.
- Storage/config mismatch. The process is pointed at the wrong database, project, or namespace: nautilus_trader claims against an empty or reset Postgres cache, next-auth's Firebase adapter reads a different Firestore database than the one written, OpenProject references an SSO slug not configured in this instance.
What usually fixes it
- [object Object]
- [object Object]
- [object Object]
- [object Object]
- [object Object]
- [object Object]
Go deeper
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
- HTTP status errors: handling 4xx and 5xx responses — how to handle 4xx and 5xx responses properly.
Documented occurrences
- Failed to load container object for inline comment. (phacility/phabricator)
- Unknown record type (we-promise/sure)
- Memory ${memoryId} was not found. (affaan-m/ECC)
- record_not_found: The requested resource was not found (we-promise/sure)
- not_found: Transaction not found (we-promise/sure)
- AuthProvider with slug: "#{slug}" has not been found (opf/openproject)
- record_not_found: The requested resource was not found (we-promise/sure)
- Failed to load comment "%s". (phacility/phabricator)
- record_not_found: The requested resource was not found (we-promise/sure)
- Custom_User_Status_Error_Invalid_User_Status: Invalid user status (RocketChat/Rocket.Chat)
- Cron job '{}' not found (zeroclaw-labs/zeroclaw)
- File not found (Mintplex-Labs/anything-llm)
- Cannot find a dashboard with the specified slug: ":slug". (octobercms/october)
- No persisted order events found for {client_order_id} (nautechsystems/nautilus_trader)
- Unable to load inline "%s". (phacility/phabricator)
- record_not_found: Security not found (we-promise/sure)
- No task with ID "%s" matches the constraints! (phacility/phabricator)
- Not Found (forem/forem)
- Skipped #${r.num}: row no longer exists in the tracker (santifer/career-ops)
- Task source '{id}' not found. (tinyhumansai/openhuman)
…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.