Exception catalogue¶
Every exception the package throws extends one abstract base, Vusys\Bitemporal\Exceptions\TemporalException (itself a RuntimeException). Catch that to handle anything temporal; catch a specific subclass to react to one failure mode.
use Vusys\Bitemporal\Exceptions\TemporalException;
try {
$product->prices()->correct(['amount' => 12.00], validFrom: '2026-02-01');
} catch (TemporalException $e) {
// any temporal failure
}
Message wording lives in lang/en/messages.php. The :placeholder token set in each message is stable across the 1.x line; publish lang/vendor/bitemporal/{locale}/messages.php to translate or reword.
The hierarchy¶
| Exception | Thrown when | Caller error? |
|---|---|---|
TemporalConfigurationException |
a model is mis-wired — soft-deletes clash, wrong temporalEntityRelation() type, a missing relation, a disabled pivot method, a failed boot guard |
yes — fix configuration |
TemporalInvalidSpellException |
a supplied period is invalid — inverted (from >= to), zero-length, or a non-forward-dated changeEffectiveFrom |
yes — fix the dates |
TemporalMissingDimensionException |
a write can't be scoped — a pending where(), an incomplete dimension tuple, or a forDimensions/attributes conflict |
yes — see Dimensions |
TemporalOverlapException |
a write or a supplied timeline would produce overlapping valid periods | yes — the overlap is in your data |
TemporalCardinalityException |
"exactly one" was expected and none / many were found, or a pivot correction/detach has no assignment to act on | usually a data/logic issue |
TemporalWriteConflictException |
a concurrent or optimistic conflict — lock timeout, missing entity row, failed expectedCurrentAttributes, or an idempotency-key reuse with different parameters |
retry or reconcile |
TemporalUnsupportedDatabaseException |
the active engine can't support a requested feature — no btree_gist, advisory locks on SQLite, or a version below the minimum |
yes — environment/config |
TemporalOnlineDdlException |
a TemporalLens::withoutIndexes() online-DDL operation could not run — it was called inside a transaction, or a package index could not be recreated on exit |
yes — call outside a transaction / rebuild the index |
TemporalDomainException |
an internal invariant was violated, or the host clock regressed beyond tolerance | no — a package bug or environment fault; report it |
TemporalConfigurationException¶
Raised at boot or relation-resolution. Covers: Bitemporal + SoftDeletes on the same model; a temporalEntityRelation() that is not BelongsTo/MorphTo; a missing $temporalEntity; calling a disabled pivot method (attach/detach/sync); enabling database.prefer_native_ranges, which is not yet supported by the read path (nativeRangesUnsupported); and aggregated boot-guard failures. See Boot guards and lints.
TemporalCardinalityException¶
Returned by sole() as expectedOneFoundNone / expectedOneFoundMany, so a broken timeline surfaces loudly rather than returning the wrong row. Also thrown by pivot writes when correctAssignment() / detachAt() find no assignment to act on. Exposes wasNoneFound(): bool to distinguish the two empty/over-full cases.
TemporalWriteConflictException¶
The concurrency surface. expectation_failed is the optimistic-concurrency check (expectedCurrentAttributes); idempotency_conflict (idempotencyKeyReused) fires when an idempotency key is reused with different parameters; idempotencySnapshotUnreadable fires when a claimed key's stored result cannot be decoded — the write is not silently re-run; lock_timeout and entity_missing come from the locking strategy (see Configuration); lockOutsideTransaction guards the PostgreSQL advisory locker against running outside a transaction, where pg_advisory_xact_lock would provide no mutual exclusion.
TemporalOnlineDdlException¶
Raised by TemporalLens::withoutIndexes(). insideTransaction fires when it is called inside an open transaction (PostgreSQL CREATE INDEX CONCURRENTLY forbids a transaction block, and dropping indexes mid-transaction is unsafe) — call it outside any transaction; the callback may open its own. recreateFailed fires when a dropped package index cannot be recreated on exit, naming the index and the DDL; on PostgreSQL a failed CREATE INDEX CONCURRENTLY may leave an INVALID index to drop manually.
TemporalDomainException¶
The only exception that is not a caller error. It signals a "should never happen" invariant breach inside the algorithm (invariant(...)), or a clock that has gone backwards beyond writes.clock_skew_tolerance_ms (clockSkew(...)). Treat it as a bug report — capture the message and reproduction.
Factory methods¶
Every subclass is final, has no public constructor, and is built through named static
factory methods — the factory name plus the message encode the scenario (there is no numeric
code). The full set, verified in sync with the code by ExceptionCatalogueParityTest:
| Exception | Factory methods |
|---|---|
TemporalConfigurationException |
missingTemporalEntity, unexpectedEntityArgument, disabledPivotMethod, nativeRangesUnsupported, guardFailures, appGuardFailures |
TemporalInvalidSpellException |
fromAfterTo, zeroLength, mergeDisjoint, antiRowCorrection, emptyTimelineSpan, unparseableDate |
TemporalMissingDimensionException |
pendingWhere, forbiddenAttribute, incomplete, unknownDimension, conflict |
TemporalOverlapException |
betweenSegments, afterBackfillAudit |
TemporalCardinalityException |
expectedOneFoundMany, expectedOneFoundNone, noAssignmentToCorrect, noAssignmentToDetach |
TemporalWriteConflictException |
entityMissing, clockRegressed, lockTimeout, deadlock, connectionChanged, lockOutsideTransaction, expectationFailed, idempotencyKeyReused, idempotencySnapshotUnreadable |
TemporalUnsupportedDatabaseException |
btreeGistMissing, advisoryLocksUnsupported, engineVersionBelowMinimum |
TemporalOnlineDdlException |
insideTransaction, recreateFailed |
TemporalDomainException |
invariant, clockSkew |
Asserting on exceptions in tests¶
The testing trait wraps the common cases so you don't hand-roll expectException plus a message match — see Testing: