zanith

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.

BEGIN
Create User

INSERT

Create Post

INSERT

COMMIT

all succeed

If any step throws an error, PostgreSQL automatically rolls back all changes:

BEGIN
Create User

INSERT ✓

Create Post

ERROR ✗

ROLLBACK

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.

TSatomic user + post creation
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:

TS
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:

TS
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