Transactions
Prisma Client supports two types of transactions: batch transactions and interactive transactions.
Batch Transactions
Execute multiple operations in a single database transaction:
Basic Batch Transaction
const [ user , post ] = await prisma . $transaction ([
prisma . user . create ({
data: { email: '[email protected] ' }
}),
prisma . post . create ({
data: { title: 'My First Post' }
})
])
// All operations succeed or all fail
How it works:
All operations execute within a single database transaction
If any operation fails, all changes are rolled back
Returns array of results in the same order
Source: /home/daytona/workspace/source/packages/client/src/runtime/getPrismaClient.ts:705-728
Array Must Contain PrismaPromises
// ❌ Error: Elements must be Prisma Client promises
await prisma . $transaction ([
Promise . resolve ( 123 ), // Error!
someAsyncFunction () // Error!
])
// ✓ Correct
await prisma . $transaction ([
prisma . user . findMany (),
prisma . post . create ({ data: { title: 'Hello' } })
])
Important: Do not await the operations before passing them:
// ❌ Wrong: Already awaited
const user = await prisma . user . create ({ data: { email: '[email protected] ' } })
await prisma . $transaction ([ user ]) // Error!
// ✓ Correct: Pass the promise
await prisma . $transaction ([
prisma . user . create ({ data: { email: '[email protected] ' } })
])
Transaction Options (Batch)
await prisma . $transaction (
[
prisma . user . create ({ data: { email: '[email protected] ' } }),
prisma . post . create ({ data: { title: 'Hello' } })
],
{
isolationLevel: 'Serializable' , // Transaction isolation level
maxWait: 5000 , // Max ms to wait for transaction start
timeout: 10000 // Max ms transaction can run
}
)
Interactive Transactions
Execute multiple operations with conditional logic:
Basic Interactive Transaction
const result = await prisma . $transaction ( async ( tx ) => {
// Create user
const user = await tx . user . create ({
data: { email: '[email protected] ' }
})
// Create post for that user
const post = await tx . post . create ({
data: {
title: 'My First Post' ,
authorId: user . id
}
})
return { user , post }
})
// Committed automatically if callback succeeds
// Rolled back automatically if callback throws
Source: /home/daytona/workspace/source/packages/client/src/runtime/getPrismaClient.ts:736-821
Conditional Logic
await prisma . $transaction ( async ( tx ) => {
const user = await tx . user . findUnique ({
where: { email: '[email protected] ' }
})
if ( ! user ) {
throw new Error ( 'User not found' )
}
if ( user . balance < 100 ) {
throw new Error ( 'Insufficient balance' )
}
// Deduct from user
await tx . user . update ({
where: { id: user . id },
data: { balance: { decrement: 100 } }
})
// Create purchase record
await tx . purchase . create ({
data: {
userId: user . id ,
amount: 100
}
})
})
Error Handling
try {
await prisma . $transaction ( async ( tx ) => {
await tx . user . create ({
data: { email: '[email protected] ' }
})
// This will throw if email is not unique
await tx . user . create ({
data: { email: '[email protected] ' }
})
})
} catch ( error ) {
// Transaction rolled back
console . error ( 'Transaction failed:' , error )
}
Explicit rollback:
await prisma . $transaction ( async ( tx ) => {
const user = await tx . user . create ({
data: { email: '[email protected] ' }
})
if ( someCondition ) {
throw new Error ( 'Rollback!' ) // Throws to rollback
}
// More operations...
})
Transaction Client
The tx parameter is a transaction-aware Prisma Client:
await prisma . $transaction ( async ( tx ) => {
// All operations use tx, not prisma
await tx . user . create ({ data: { email: '[email protected] ' } })
await tx . post . create ({ data: { title: 'Hello' } })
// ❌ Don't mix transaction and non-transaction clients
await prisma . comment . create ({ data: { text: 'Hi' } }) // Runs OUTSIDE transaction!
})
Transaction client restrictions:
Certain operations are not available within transactions:
await prisma . $transaction ( async ( tx ) => {
// ❌ These don't work in transactions
await tx . $transaction ([]) // Error: Cannot nest $transaction
tx . $on ( 'query' , () => {}) // Error: $on not available
await tx . $disconnect () // Error: Cannot disconnect within transaction
})
Source: Transaction client deny list at /home/daytona/workspace/source/packages/client/src/runtime/core/types/exported/itxClientDenyList.ts
Transaction Options (Interactive)
await prisma . $transaction (
async ( tx ) => {
// Transaction logic
},
{
maxWait: 5000 , // Max ms to wait for transaction start (default: 2000)
timeout: 10000 , // Max ms transaction can run (default: 5000)
isolationLevel: 'ReadCommitted' // Transaction isolation level
}
)
Maximum time (ms) to wait for a transaction slot to become available. If all connections are busy, the transaction waits up to this duration.
Maximum time (ms) the transaction can run before it’s automatically rolled back. await prisma . $transaction (
async ( tx ) => {
await tx . user . create ({ data: { email: '[email protected] ' } })
await delay ( 6000 ) // Exceeds timeout!
},
{ timeout: 5000 }
)
// Throws: P2028 - Transaction expired
Controls transaction isolation level. Options:
ReadUncommitted
ReadCommitted (default for most databases)
RepeatableRead
Snapshot (SQL Server)
Serializable
await prisma . $transaction (
async ( tx ) => { /* ... */ },
{ isolationLevel: 'Serializable' }
)
Availability varies by database provider.
Source: Transaction options defined at /home/daytona/workspace/source/packages/client/src/runtime/getPrismaClient.ts:97-102,422-426,765-770
Nested Transactions (Savepoints)
Prisma supports nested interactive transactions using database savepoints:
await prisma . $transaction ( async ( tx1 ) => {
await tx1 . user . create ({ data: { email: '[email protected] ' } })
// Nested transaction
await tx1 . $transaction ( async ( tx2 ) => {
await tx2 . post . create ({ data: { title: 'Post 1' } })
await tx2 . post . create ({ data: { title: 'Post 2' } })
// If this throws, only inner transaction rolls back
})
// User is still created
})
How savepoints work:
Outer transaction starts (BEGIN)
Inner transaction creates savepoint (SAVEPOINT sp1)
If inner transaction throws, rollback to savepoint (ROLLBACK TO sp1)
If inner succeeds, release savepoint (provider-specific)
Outer transaction commits (COMMIT)
Source: Savepoint transaction management at /home/daytona/workspace/source/packages/client/src/runtime/getPrismaClient.ts:743-821
Important constraints:
Nested transactions must be closed in reverse order
MongoDB does not support nested transactions
Cloudflare D1 does not support interactive transactions at all
// ❌ Error: Must close in reverse order
await prisma . $transaction ( async ( tx1 ) => {
const promise = tx1 . $transaction ( async ( tx2 ) => {
// Inner transaction
})
// Don't await yet - do other work
await tx1 . user . create ({ data: { email: '[email protected] ' } })
await promise // Error: Must close nested tx before continuing
})
Timeout Behavior
Transactions have two timeout scenarios:
Start Timeout (maxWait)
If no connection is available:
try {
await prisma . $transaction (
async ( tx ) => { /* ... */ },
{ maxWait: 2000 } // Wait max 2s for connection
)
} catch ( error ) {
// Timeout waiting for connection
}
Execution Timeout (timeout)
If transaction runs too long:
const result = prisma . $transaction (
async ( tx ) => {
await tx . user . create ({ data: { email: '[email protected] ' } })
await delay ( 6000 ) // Exceeds timeout
},
{ timeout: 5000 }
)
await expect ( result ). rejects . toMatchObject ({
code: 'P2028' ,
message: expect . stringMatching ( /transaction . * expired/ i )
})
Source: Transaction timeout test at /home/daytona/workspace/source/packages/client/tests/functional/interactive-transactions/tests.ts:68-88
Default Transaction Options
Set default options when creating the client:
const prisma = new PrismaClient ({
adapter ,
transactionOptions: {
maxWait: 3000 ,
timeout: 10000 ,
isolationLevel: 'ReadCommitted'
}
})
// These defaults apply to all transactions
await prisma . $transaction ([ ... ])
You can override defaults per transaction:
await prisma . $transaction (
async ( tx ) => { /* ... */ },
{ timeout: 15000 } // Override default
)
Provider-Specific Behavior
PostgreSQL
Supports all isolation levels
Savepoint syntax: SAVEPOINT name, ROLLBACK TO SAVEPOINT name, RELEASE SAVEPOINT name
Nested transactions fully supported
MySQL
Supports: ReadUncommitted, ReadCommitted, RepeatableRead, Serializable
Savepoint syntax: SAVEPOINT name, ROLLBACK TO name (no RELEASE)
Nested transactions supported
SQLite
Limited isolation level support
Savepoint syntax: SAVEPOINT name, ROLLBACK TO name, RELEASE name
Nested transactions supported
SQL Server
Supports: ReadUncommitted, ReadCommitted, RepeatableRead, Snapshot, Serializable
Savepoint syntax: SAVE TRANSACTION name, ROLLBACK TRANSACTION name (no release)
Nested transactions supported
MongoDB
Limited isolation level support
Nested transactions not supported
Interactive transactions supported at top level
Cloudflare D1
Interactive transactions not supported
Only batch transactions available
// ❌ Error with D1 adapter
await prisma . $transaction ( async ( tx ) => {
// Error: D1 does not support interactive transactions
})
// ✓ OK with D1
await prisma . $transaction ([
prisma . user . create ({ data: { email: '[email protected] ' } })
])
Source: D1 transaction restriction at /home/daytona/workspace/source/packages/client/src/runtime/getPrismaClient.ts:858-862
Transaction Scope and Context
Interactive transactions maintain context across async boundaries:
async function createUserAndProfile ( tx : any , email : string ) {
const user = await tx . user . create ({ data: { email } })
await tx . profile . create ({ data: { userId: user . id } })
return user
}
await prisma . $transaction ( async ( tx ) => {
// Pass tx to helper functions
const user = await createUserAndProfile ( tx , '[email protected] ' )
await createUserAndProfile ( tx , '[email protected] ' )
})
Source: Transaction context handling at /home/daytona/workspace/source/packages/client/src/runtime/getPrismaClient.ts:238-251,743-761,823-845
Best Practices
Keep Transactions Short
// ❌ Bad: Long-running transaction
await prisma . $transaction ( async ( tx ) => {
const users = await tx . user . findMany () // Could be thousands
for ( const user of users ) {
await sendEmail ( user . email ) // External API call!
await tx . user . update ({ where: { id: user . id }, data: { notified: true } })
}
})
// ✓ Good: Minimize transaction scope
const users = await prisma . user . findMany ()
for ( const user of users ) {
await sendEmail ( user . email )
}
await prisma . $transaction (
users . map ( user =>
prisma . user . update ({ where: { id: user . id }, data: { notified: true } })
)
)
Avoid External Calls in Transactions
// ❌ Bad
await prisma . $transaction ( async ( tx ) => {
const user = await tx . user . create ({ data: { email: '[email protected] ' } })
await fetch ( 'https://api.example.com/notify' ) // External call
await tx . post . create ({ data: { authorId: user . id , title: 'Hello' } })
})
// ✓ Good: External calls outside transaction
await prisma . $transaction ( async ( tx ) => {
const user = await tx . user . create ({ data: { email: '[email protected] ' } })
await tx . post . create ({ data: { authorId: user . id , title: 'Hello' } })
})
await fetch ( 'https://api.example.com/notify' )
Use Batch When Possible
// Interactive transaction for simple operations
await prisma . $transaction ( async ( tx ) => {
await tx . user . create ({ data: { email: '[email protected] ' } })
await tx . post . create ({ data: { title: 'Hello' } })
})
// ✓ Better: Use batch transaction (simpler, less overhead)
await prisma . $transaction ([
prisma . user . create ({ data: { email: '[email protected] ' } }),
prisma . post . create ({ data: { title: 'Hello' } })
])
Handle Specific Errors
try {
await prisma . $transaction ( async ( tx ) => {
// Transaction logic
})
} catch ( error ) {
if ( error . code === 'P2028' ) {
console . error ( 'Transaction timeout' )
} else if ( error . code === 'P2034' ) {
console . error ( 'Transaction conflict' )
} else {
console . error ( 'Transaction failed:' , error )
}
}
Next Steps
Middleware Hook into the query execution pipeline
Error Handling Handle transaction errors and conflicts