Thrown when the database rejected a write because a concurrent transaction changed the same data (X38-1, M90f). The operation did not happen and may be retried.
The condition reaches the client as 409 Conflict — the canonical
retryable-signal status — instead of the masked 500 that told a
well-behaved client to give up, silently dropping the write and making
optimistic concurrency unusable. The original driver error is preserved as
cause, so the operator's diagnostic still reaches the SQLSTATE (M90j
keeps that reachable), while the served detail is the fixed sentence
here — never the driver message, which on a pg error quotes the failing
statement (X12-3 stays closed).
The detail is also the answer to "what would a caller have been relying
on before": a masked 500 carrying Internal Server Error and nothing
else, which is not a behaviour any caller can depend on.
Example 1
Example 1
import { SerializationConflictError } from '@setu-ts/database-plugin'; try { await db.transaction(async (uow) => { const row = await uow.getRepository('Account').findById(id); await uow.getRepository('Account').update(id, { balance: row.balance - 10 }); }); } catch (err) { if (err instanceof SerializationConflictError) { // Retry: the transaction rolled back without applying. } }
Creates the error. The message is the full diagnostic — safe to log,
never to serve.
name: string
Discriminant for consumers that cannot use instanceof across realms.