Transactions
Run multiple operations atomically — if anything fails, everything rolls back. The transaction context gives you the same API as the main client.
Why transactions?
Imagine you're creating a user and their first post. If the post creation fails (say, a constraint violation), you don't want a user record sitting in the database with no post. A transaction ensures both operations succeed or neither does.
INSERT
INSERT
all succeed
If any step throws an error, PostgreSQL automatically rolls back all changes:
INSERT ✓
ERROR ✗
user removed
Basic usage
Inside the transaction callback, you get the same API as the main client — model queries, inserts, raw SQL, everything works. The only difference is everything runs on the same database connection.
await db.transaction(async (tx) => { // Create the user const user = await tx.user.create({ data: { email: '[email protected]', name: 'Alice' }, }); // Create their first post (uses the user's ID) await tx.post.create({ data: { title: 'Hello World', authorId: user.id, // guaranteed to exist }, }); // Raw SQL works inside transactions too await tx.raw` UPDATE counters SET n = n + 1 WHERE name = 'users' `; // If ANY operation throws, EVERYTHING rolls back automatically});Error handling
If the callback throws, the transaction is rolled back and the error is re-thrown. You can catch it outside:
try { await db.transaction(async (tx) => { await tx.user.create({ data: { email: '[email protected]' } }); // If email already exists → UniqueConstraintError });} catch (err) { if (err instanceof UniqueConstraintError) { console.log('Email already taken:', err.fields); } // The transaction was already rolled back at this point}What's available inside tx
The transaction context (tx) provides the same interface as the maindb client:
await db.transaction(async (tx) => { // Model CRUD await tx.user.findMany({ where: { role: 'ADMIN' } }); await tx.user.create({ data: { ... } }); await tx.user.update({ where: { id: '...' }, data: { ... } }); await tx.user.delete({ where: { id: '...' } }); // Insert builder await tx.user.insert({ email: '[email protected]' }) .onConflict({ columns: ['email'], action: 'nothing' }) .execute(); // Raw SQL await tx.raw`SELECT COUNT(*) FROM users`;});When to use transactions
Use transactions when:
- Multiple related writes — creating a user and their profile together
- Transfer operations — deducting from one account and adding to another
- Conditional updates — check a value, then update based on it (no race condition)
- Batch operations — processing a list of items where all must succeed