20. Transaction outcomes
Resource, relationship, bulk and canonical registry write errors carry the outcome of the transaction they represent. HTTP and non-atomic bulk errors preserve that outcome. The states below describe the evidence available when the failure is reported.
Managed transactions explains ownership and completion ordering. These guarantees cannot recover an error swallowed by an application extension.
The five states
| State | Exact meaning |
|---|---|
none |
No transaction was acquired or accepted for this operation. This includes rejection before transaction setup. It does not certify that external or nontransactional work had no effects. |
pending |
The operation participated in a transaction whose owner has not completed it. The owner still must decide its outcome. A database error may have made further SQL unusable; pending does not mean the owner can safely continue writing. |
committed |
The backend confirmed commit of the represented outer transaction. Later hook, file, notification or logging failure does not change this outcome. Releasing a savepoint does not establish outer commit. |
rolledBack |
The backend confirmed rollback of the represented transaction. Later cleanup failure does not undo that confirmation. This says nothing about external effects performed before rollback. |
unknown |
A transaction was acquired or accepted, but the available evidence cannot establish its outcome or leave it confidently pending. Examples include lost completion acknowledgements, failed completion/rollback, and an accepted handle completed outside its owner without observed outcome. |
Use these values consistently. Do not add synonyms for driver states or infer them from exception messages. Ownership is separate from outcome: an individual resource operation borrows its caller’s transaction, including a transaction managed by an outer library operation.
Evidence and transitions
Transaction setup starts with none. Successful acquisition or acceptance of
an unfinished transaction moves to pending. An operation that merely borrows
that transaction leaves it pending; its own success or rejection does not commit
or roll back the outer unit. A rejected statement may require the owner to roll
back even if the driver’s handle remains usable-looking.
The private transaction owner controls completion; a hook changing the mutable
shouldCommit diagnostic flag neither takes ownership nor suppresses an implicit
owner’s commit or rollback. Participating operation errors stay pending snapshots
even when the outer helper later reports rolledBack.
Confirmed commit or rollback establishes committed or rolledBack. A failed
completion attempt without confirmation establishes unknown. If a subsequent
rollback is actually confirmed, the reported outcome can be rolledBack; a
cleanup attempt or a resolved wrapper alone is insufficient. Once commit is
confirmed, later failure remains committed and must not trigger rollback.
Knex 3.1.0 exposes a specific trap: commit() and rollback() can resolve even
when their control query failed, while executionPromise rejects. Its
isCompleted() flag is set when completion starts and after failures; the flag
alone proves neither commit nor rollback. The current shared completion helpers
now await executionPromise before reporting successful completion to hooks or
the registry. Driver-path regressions cover rejected SQL and injected lost
acknowledgements after real execution.
The shared helper also rejects a PostgreSQL COMMIT response reporting ROLLBACK. Both Knex promises can resolve in this case, but the backend has confirmed that the writes were rolled back. The existing failure path consumes that confirmation once, without sending another ROLLBACK. Resource rollback hooks can then clean uploaded files; commit hooks and registry cache publication do not run.
Owned completion now requires a top-level transaction. A custom transaction factory returning a savepoint is rejected before release; the existing failure path rolls back that savepoint and leaves its caller-owned parent active. No commit hook or committed registry cache publication follows that rejection. The managed implementation rejects raw transactions and savepoints supplied to library writes before acceptance, with outcome none. A managed handle completed outside its owner after acceptance instead has outcome unknown. Connection failure during commit must remain unknown unless available evidence establishes the outcome. The error remains a snapshot even if the application later reconciles the database state.
When commit acknowledgement is lost
A connection can fail before COMMIT reaches the database or after the database
commits but before the caller receives confirmation. A rejected promise or HTTP
500 alone cannot distinguish those cases. unknown deliberately preserves that
uncertainty; it is not a synonym for rolledBack.
| Observed evidence | Reported outcome and behavior |
|---|---|
| Commit and driver completion acknowledged, then an after-commit hook fails | committed; retain the failure, never attempt rollback of the committed write. |
| Commit control statement or its completion promise rejects without acknowledged outcome | unknown; no speculative commit/rollback hooks. A driver’s completed flag alone does not establish success. |
| PostgreSQL acknowledges COMMIT as ROLLBACK | rolledBack; run rollback completion work without issuing a second rollback. |
| Rollback is acknowledged, then cleanup fails | rolledBack; retain the primary write error and secondary cleanup diagnostics. |
| Rollback itself cannot be acknowledged | unknown; keep the original failure and rollback diagnostic. |
The Knex storage plugins and owned AnyAPI registry operations retain the physical connection lease until the transaction completion promise settles. A failed completion marks that connection disposed before releasing it, so even a request already waiting for the pool cannot borrow its unfinished transaction. A failed BEGIN also rejects before the application callback runs. Acknowledged completion keeps normal connection reuse.
Owned PostgreSQL alterKnexFields also uses the managed completion runner.
A deep-helper call given an existing transaction uses a savepoint and leaves
completion to that caller. SQLite table rebuilds are Knex-managed internally;
the library holds their connection lease through error cleanup, rolling back a
transaction the driver reports still active and restoring foreign-key enforcement
before release. If cleanup cannot establish a reusable connection, it is disposed.
See the schema guide for DDL-specific
ownership, cleanup errors and dialect restrictions.
If a completion method rejects without settling its driver completion promise,
an owner finishing with outcome unknown also discards its unreleased connection
lease. A later driver promise settlement cannot release the lease twice. The raw
driver promise may remain unsettled; application completion follows the library
call and its outcome, rather than waiting on that internal promise.
Connection release is cleanup, separate from the driver’s SQL-completion promise.
A release error after acknowledged COMMIT keeps outcome committed; it becomes a
post-commit error and later completion hooks/finalizers are still attempted. A
release error after confirmed rollback keeps rolledBack, preserves the original
failure, and adds a connectionRelease diagnostic to the owner’s cleanup errors.
Failed BEGIN also retains its original cause if releasing its lease fails.
Disposal does not establish whether a write committed, and does not replay it.
Knex destroys the disposed connection during pool validation or idle cleanup;
this is not a guarantee of immediate server-side lock release. SQLite :memory:
data belongs to its connection and is lost when that connection is discarded;
use a file-backed database when data must survive connection replacement.
After unknown, reconcile using application-owned operation identity and
persisted state before deciding whether to issue a new write. Do not treat a
previously captured participant error as the owner’s final outcome. Do not infer
that files in upload tracking are safe to delete: they may belong to committed
rows. Outcome-dependent hooks are skipped when the outcome is unknown, so
notifications and other in-memory completion work may require reconciliation as
well. There is no durable outbox, automatic retry or exactly-once delivery.
Database rollback does not undo email, remote calls or other effects performed
outside that database. Even rolledBack permits a safe retry only when the
application can repeat or reconcile those effects. See the
file ownership contract.
The completion tests inject failures at the actual driver query boundary before and after executing COMMIT/ROLLBACK, inspect persisted rows afterward, and assert no outcome hooks run for unknown completion. They cover real supported databases, but do not simulate every network partition, server failover or physical connection-loss sequence. This contract reports available evidence; it does not promise to resolve an acknowledgement the client never received.
Selected error carrier
Writes reject with RestApiWriteError, exported from the package root. Its
read-only transactionOutcome is one of the five strings above. cause retains
the original thrown value, including a frozen error, null or undefined; a fresh
wrapper is created for each failed operation. Read errors retain their existing
contract. The runtime propagates null and undefined throws. Errors already lost
inside an extension’s own catch cannot be restored by the write boundary.
The wrapper retains ordinary data fields used for classification: code,
type, subtype, details, statusCode, status, path, parameter and
validation, including inherited status fields from HTTP error classes. It does
not execute diagnostic getters. Without an original code,
its code is REST_API_WRITE. The original error class is available through
cause; the wrapper itself has class RestApiWriteError. Its JSON representation
omits the cause, stack and transaction handle while retaining diagnostic fields.
An atomic child’s wrapper can be the outer wrapper’s cause. Both snapshots are
retained: for example, child pending and outer rolledBack. Existing code/subtype
classification can usually remain at the outer error; code that checks original
error identity or instanceof needs migration. HTTP mapping preserves status
classification and adds meta.transactionOutcome to each JSON:API error.
Non-atomic bulk entries expose error.transactionOutcome individually.
Errors, batches and later observations
Outcome evidence must survive a secondary cleanup error without replacing the primary failure or its cause. The selected wrapper carries frozen errors and non-Error throws without mutating the original value. Completion hooks and internal diagnostic finalizers are attempted in their documented order despite earlier completion failures. After commit, the first such failure remains primary. Finalizer errors after rollback or unknown completion remain secondary; they do not change the observed outcome or cause speculative outcome hooks to run. See the managed completion contract. Only safe, serializable outcome fields belong in transport errors. Transaction, connection, storage and raw cleanup objects remain internal diagnostics.
An outcome attached to an error is a snapshot, not a live view. A borrowed operation can report pending and its owner later report rolledBack. Later confirmation belongs to the owner’s completion result; do not mutate previously reported failures or their causes to rewrite that history.
An atomic batch has one transaction outcome. A non-atomic batch has separate
outcomes for its entries; it must not claim one shared committed/rolledBack
outcome from aggregate success counts. Its meta.failed counts rejected calls,
including possible post-commit failures. Each rejected entry carries its own
outcome; mixed results do not introduce a sixth transaction state.
None of these states is a general retry instruction. Repeating a committed or unknown write can duplicate effects. Pending work belongs to its owner. Even a confirmed rollback cannot undo an email, upload, external API call or other effect outside the database transaction. No automatic replay of arbitrary hooks or setters is part of this contract.
Dispatcher failures
Resource dispatch and hook execution preserve the original thrown value, including null and undefined. The runtime does not add automatic failure logging or wrap plugin installation errors. The transaction layer attaches write outcomes and records failed cleanup diagnostics without replacing the primary cause.