Error Handling
Typed error classes for validation, not-found, conflict, failed-precondition, malformed-id,
pagination-cursor, and missing-index failures, plus the parseFirestoreError normalizer. The
drop-in Express middleware that maps these to HTTP responses lives in
Express integration.
Overview
Section titled “Overview”The ORM throws a small set of typed errors so you can branch on failure cause instead of parsing
strings. Every error extends the built-in Error, so instanceof checks work as expected:
import { ValidationError, NotFoundError, ConflictError, PreconditionFailedError, FirestoreIndexError, InvalidDocumentIdError, InvalidPaginationCursorError, WriteOutcomeError,} from 'flintfire';
try { await userRepo.create(invalidData);} catch (error) { if (error instanceof ValidationError) { // Handle validation errors error.issues.forEach(issue => { console.log(`${issue.path}: ${issue.message}`); }); } else if (error instanceof InvalidDocumentIdError) { // Handle a malformed document id console.log(`Invalid document id (${error.reason})`); } else if (error instanceof InvalidPaginationCursorError) { // Ask the client to restart pagination from the first page console.log(`Invalid pagination cursor (${error.reason})`); } else if (error instanceof NotFoundError) { // Handle not found console.log('Document not found'); } else if (error instanceof ConflictError) { // A create-only write lost the race for that id console.log('Document already exists'); } else if (error instanceof PreconditionFailedError) { // Someone else wrote the document since we read it — re-read and retry console.log('Stale write — document changed underneath us'); } else if (error instanceof FirestoreIndexError) { // Handle missing composite index console.log(error.toString()); // Includes link to create index }}Raw Firestore errors (for example a missing composite index) are normalized into these classes by
parseFirestoreError before they reach your catch block — see
Normalizing raw Firestore errors below.
Error classes
Section titled “Error classes”ValidationError
Section titled “ValidationError”Thrown when Zod schema validation fails on a write (create, bulkCreate, update, patch,
upsert, and their transaction/query-builder equivalents). On bulkWrite, the same failure arrives
inside BulkWriteResult.error for that item rather than rejecting the whole call.
Properties:
issues: ZodIssue[]— the array of underlying Zod validation issues (each haspathandmessage)message: string— a formatted summary built from the issues (each rendered aspath: message, comma-joined), e.g.email: Invalid email address, age: Too small: expected number to be >0. The message text is produced by Zod, so it varies by Zod version and any custom messages you set.
InvalidDocumentIdError
Section titled “InvalidDocumentIdError”Thrown when a document id is malformed. Every id-taking surface validates its id before touching
Firestore — repo.id(raw), getById, update, patch, upsert, delete, the bulk* methods,
their *InTransaction equivalents, whereId, and whereFilter’s f.whereId — and rejects an id
that is not a string, is empty, contains /, is . or .., is wrapped in __…__, exceeds 1500
UTF-8 bytes, or contains invalid UTF-16 (a lone surrogate). On
bulkWrite, a malformed id is reported per item in BulkWriteResult.error (siblings still write);
recursiveDelete still throws this error for a bad id. See
Document Identity.
Properties:
reason: InvalidDocumentIdReason— a discriminant describing why the id was rejectedmessage: string— error description
InvalidPaginationCursorError
Section titled “InvalidPaginationCursorError”Thrown by paginate / paginateWithCount when an opaque cursor cannot safely continue the query.
The cursor may be malformed, may identify a document outside the current collection or collection
group, or may point to a document deleted since the previous page. Consumers should restart from the
first page or return a client error rather than matching the human-readable message.
Properties:
reason: InvalidPaginationCursorReason—'malformed' | 'source_mismatch' | 'stale'message: string— a stable, non-sensitive description of the category
The error never stores the cursor token, decoded document path, or parser failure. This keeps logs
and HTTP serialization from reflecting untrusted cursor contents. The Express adapter maps it to
HTTP 400 and returns only { error: 'InvalidPaginationCursorError', reason }.
NotFoundError
Section titled “NotFoundError”Thrown when a document that must exist is missing. Specifically:
- the
*OrThrowreads —getByIdOrThrow(id)andgetOneByFieldOrThrow(field, value)(the latter when no document matches) delete(id)on a document that does not exist — including when alastUpdateTimeprecondition was supplied, because the existence pre-read runs before the guarded write
It is also the normalized form of a raw Firestore not-found error (see parseFirestoreError). On
bulkWrite, a backend not-found (e.g. update on a missing id) lands in that item’s
BulkWriteResult.error rather than throwing for the whole call.
Properties:
message: string— error description, e.g.Document with id user-123 not found
ConflictError
Section titled “ConflictError”Thrown when:
- a create-only write loses —
createWithId,bulkCreateWithIds, orcreateWithIdInTransactiontargeting an ID that already exists. This is the normalized form of a raw Firestorealready-existserror (gRPC code6), and the check is atomic on the backend, so of two concurrent creates on one ID exactly one wins and the other gets this error. getOneByFieldOrThrow(field, value)matches more than one document — the method expects exactly one.
It is also a convenient error to throw yourself when enforcing uniqueness or other business rules in
application code. On bulkWrite, a colliding create / set surfaces as that item’s
BulkWriteResult.error (ConflictError) while siblings may still land.
Properties:
message: string— error description
PreconditionFailedError
Section titled “PreconditionFailedError”Thrown when a write’s lastUpdateTime precondition did not hold — the document was modified (or
removed) by someone else since the version you read. This is the lost-update signal for optimistic
concurrency, and it is the normalized form of a raw Firestore failed-precondition error (gRPC code
9) that is not a missing-index error.
The rejected write is never applied, so the stored document is exactly what the other writer left —
a retry against a freshly-read token is always safe. See
Conditional writes.
On bulkWrite, the same error arrives inside BulkWriteResult.error for that item.
Two neighbouring cases are deliberately not this error:
- a create-only collision is
ConflictError(gRPC6), not a precondition failure; - a missing document is
NotFoundErroronly when no precondition was supplied. With alastUpdateTime, Firestore reports the absent document as stored version 0, soupdate(id, data, { lastUpdateTime })on a deleted document raisesPreconditionFailedError. (delete(id, { lastUpdateTime })still raisesNotFoundError, becausedeleteperforms its own existence pre-read first.)
Properties:
message: string— error description
FirestoreIndexError
Section titled “FirestoreIndexError”Thrown when a query requires a composite index that does not exist yet. The error carries the Firebase console URL that creates the required index automatically.
Properties:
indexUrl: string— URL to create the required indexfields: string[]— the fields that require indexingtoString(): string— returns a formatted, human-readable message with the index URL and setup instructions
Write outcomes (WriteOutcomeError)
Section titled “Write outcomes (WriteOutcomeError)”Outcome-sensitive write failures — a before* / after* hook threw, a later fixed-batch chunk
failed after earlier chunks committed, or a postcommit { returnDoc: true } read/converter failed —
surface as WriteOutcomeError. The original failure is preserved as cause. Ordinary
validation, malformed-id, not-found, conflict, and precondition errors remain their existing
top-level classes when no write committed and no hook is the failed phase.
outcome is a discriminated union (cause is not inside outcome, so HTTP serialization stays
safe):
state |
phase |
Meaning |
|---|---|---|
not-committed |
before-hook |
A before* hook failed; no write for this call |
partially-committed |
commit |
Some fixed-batch writes committed (committedWrites / totalWrites) |
committed |
after-hook |
Write committed; an after* hook failed |
committed |
read-back |
Write committed; { returnDoc: true } converter/read failed |
try { await userRepo.create(data);} catch (error) { if (error instanceof WriteOutcomeError) { switch (error.outcome.state) { case 'not-committed': // Firestore write did not commit. Earlier before-hooks may still have // delivered external side effects — retry only with an idempotent // business/write identity. break; case 'partially-committed': console.log(error.outcome.committedWrites, error.outcome.totalWrites); break; case 'committed': // data is persisted — handle after-hook / read-back separately break; } }}Key side effects by a business / write identity stored atomically with the data — not by
HookContext.attempt (diagnostic only). Durable after-hook delivery is tracked as
#80.
parseFirestoreError preserves an existing WriteOutcomeError unchanged before SDK
normalization.
Normalizing raw Firestore errors
Section titled “Normalizing raw Firestore errors”import { parseFirestoreError } from 'flintfire';parseFirestoreError(error: unknown): Error
Normalizes a raw error thrown by the Firestore SDK into one of the ORM’s typed errors. The repository and query builder call this internally on every operation, so you normally never invoke it directly — the errors you catch are already normalized. It maps:
- a Firestore
not-founderror (gRPC code5) →NotFoundError - an index-required error (gRPC code
9whose details mentionrequires an index) →FirestoreIndexError, withindexUrlandfieldsextracted from the error details - an
already-existserror (gRPC code6) →ConflictError - any other
failed-preconditionerror (gRPC code9) →PreconditionFailedError - any other
Error→ returned unchanged; a non-Errorvalue (a string or plain object) is wrapped in a newError
The index-required branch is checked before the general code-9 branch, so a missing-index
failure is still a FirestoreIndexError and never a PreconditionFailedError. Both the numeric
code (6, 9) and the string form ('already-exists', 'failed-precondition') are recognized.
Mapping errors to HTTP responses
Section titled “Mapping errors to HTTP responses”The ORM ships a pre-built Express middleware that maps these error classes to HTTP status codes and
JSON bodies. It is published from the optional flintfire/express subpath and
is documented, with the full status-code and response-body tables, in
Express integration.