Does Prisma roll back all writes in a transaction callback after a partial failure?
0 reputation · 27 Oct 2024, 11:21 UTC
0 reputation · 27 Oct 2024, 11:21 UTC
Goal: Determine whether Prisma Client’s $transaction callback provides full atomicity when an error is thrown after some writes have already succeeded within the same transaction scope.
Constraints: The transaction API reuses a single connection for the callback, uses SAVEPOINTs for nested writes on supported databases, and automatically rolls back on any thrown error, but the documentation does not specify if writes that succeeded before the error are undone. Additionally, behavior may vary between Prisma versions (≥4.0) and between PostgreSQL and MySQL due to differing rollback granularity.
Uncertainty: It is unclear whether Prisma guarantees that all operations in the callback are rolled back or only those after the point of failure, and whether dialect‑specific savepoint handling affects this outcome.
Does Prisma automatically roll back every write in the callback when an error occurs after some writes have succeeded? Does the rollback behavior differ between PostgreSQL and MySQL when nested writes use SAVEPOINTs? Does the connection remain checked out from the pool for the entire callback duration regardless of a partial failure?
35275 reputation · 27 Oct 2024, 22:56 UTC
Yes. With an interactive transaction — prisma.$transaction(async (tx) => { ... }) — if the callback throws or rejects, Prisma aborts the transaction and the database rolls back every write made through the tx client, including statements that executed successfully before the failing one. Statements that ran earlier were never durable; they only become permanent at commit, and a thrown error prevents commit entirely. This is standard database transaction semantics, not Prisma-specific magic: Prisma opens a transaction on a dedicated connection, runs your callback against it, and issues COMMIT only if the callback resolves.
The rollback guarantee has sharp edges you should check in your own code:
prisma instead of the tx argument, that write runs on a different connection outside the transaction and will persist after rollback. This is the most common cause of "Prisma didn't roll everything back" reports.For ordinary DML (INSERT/UPDATE/DELETE), rollback behavior is equivalent on both: the whole transaction is undone. The differences are at the margins. MySQL's InnoDB implicitly commits on most DDL, so a schema-altering raw statement mid-callback can commit earlier writes permanently — PostgreSQL's transactional DDL does not have this problem. Savepoints matter mainly for nested transaction APIs: Prisma maps nested $transaction calls to savepoints on supported providers, so an inner rollback releases to the savepoint without touching the outer transaction's earlier work. An uncaught error that escapes the outermost callback still rolls back everything.
The interactive transaction pins one connection from the pool for the full callback duration, whether it ends in commit or rollback. After a failure the connection is released back to the pool once the rollback completes. This is why long callbacks risk pool exhaustion and why the transaction timeout option exists.
Don't take any version's behavior on faith — test against your actual Prisma version and provider:
try {
await prisma.$transaction(async (tx) => {
await tx.widget.create({ data: { name: 'A' } });
throw new Error('simulated failure');
// await tx.widget.create({ data: { name: 'B' } });
});
} catch {}
const count = await prisma.widget.count();
// expect 0: write A was rolled back with the aborted transactionThen repeat with one write issued via the outer prisma client inside the callback and confirm it survives — that contrast demonstrates exactly where the atomicity boundary sits. If you rely on DDL or raw statements on MySQL, add a case for those too, since connector- and engine-specific behavior there is the part most likely to surprise you.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 27 Oct 2024, 17:25 UTC
To build on the previous point regarding atomicity, it is critical to note how try...catch blocks inside the callback affect this behavior. Prisma triggers the rollback based on whether the callback function throws or rejects.
If you wrap a write operation in a try...catch block and handle the error without re-throwing it, Prisma perceives the callback as having resolved successfully. In this scenario, any writes that succeeded prior to the caught error will be committed to the database, effectively resulting in a partial success.
throw error;).PrismaClientKnownRequestError.