Skip to main content

Transactions

Group multiple operations into atomic transactions.

Basic Transactions

Use transact to wrap operations in a transaction:

await db.transact(async (tx) => {
const author = await tx.createOne("author", {
values: {
name: "Jane Austen",
email: "jane@example.com",
},
returning: ["id"],
});

await tx.createOne("book", {
values: {
title: "Pride and Prejudice",
authorId: author.id,
},
});
});
// Both author and book are committed together

If any operation fails, all changes are rolled back:

try {
await db.transact(async (tx) => {
await tx.createOne("author", {
values: {
name: "Jane Austen",
email: "jane@example.com",
},
});

// This error causes the entire transaction to roll back
throw new Error("Something went wrong");
});
} catch (error) {
// Transaction was rolled back, author was not created
}

Returning Values

Transactions can return values:

const result = await db.transact(async (tx) => {
const author = await tx.createOne("author", {
values: {
name: "Jane Austen",
email: "jane@example.com",
},
returning: ["id", "name"],
});

const book = await tx.createOne("book", {
values: {
title: "Pride and Prejudice",
authorId: author.id,
},
returning: ["id", "title"],
});

return { author, book };
});

// result.author and result.book are available
console.log(result.author.name); // "Jane Austen"
console.log(result.book.title); // "Pride and Prejudice"

Force Rollback

For testing, you can force a rollback even on success:

await db.transact(
async (tx) => {
await tx.createOne("author", {
values: {
name: "Test Author",
email: "test@example.com",
},
});

// Verify within transaction
const count = await tx.count("author", { where: {} });
console.log(count); // 1 inside the transaction
},
{ rollback: true } // Force rollback
);

// Nothing was persisted - useful for testing
const count = await db.count("author", { where: {} });
console.log(count); // 0

Nested Transactions

Nested transactions use PostgreSQL savepoints:

await db.transact(async (tx) => {
const author = await tx.createOne("author", {
values: {
name: "Jane Austen",
email: "jane@example.com",
},
returning: ["id"],
});

// Nested transaction creates a savepoint
try {
await tx.transact(async (inner) => {
await inner.createOne("book", {
values: {
title: "Failed Book",
authorId: author.id,
},
});

throw new Error("Roll back only the book");
});
} catch {
// Inner transaction rolled back to savepoint
// Author is still created
}

// Create a successful book
await tx.createOne("book", {
values: {
title: "Pride and Prejudice",
authorId: author.id,
},
});
});

// Result: Author exists with one book ("Pride and Prejudice")
// "Failed Book" was rolled back

Row Locking

Lock rows within a transaction to prevent concurrent updates:

await db.transact(async (tx) => {
// Lock the author row for update
const author = await tx.findOne("author", {
select: ["id", "name"],
where: { id: 1 },
for: "update",
});

// Other transactions will wait for this lock
await tx.updateOne("author", {
set: { name: "Updated Name" },
where: { id: author.id },
});
});

Best Practices

Keep Transactions Short

Long-running transactions hold locks and can cause contention:

// ❌ Bad: HTTP call inside transaction
await db.transact(async (tx) => {
const user = await tx.findOne("user", { ... });
await fetch("https://api.example.com/notify"); // Slow!
await tx.updateOne("user", { ... });
});

// ✅ Good: Do slow work outside transaction
const user = await db.findOne("user", { ... });
await fetch("https://api.example.com/notify");
await db.transact(async (tx) => {
await tx.updateOne("user", { ... });
});

Handle Errors Appropriately

try {
await db.transact(async (tx) => {
// Operations...
});
} catch (error) {
if (error.code === "23505") {
// Unique violation - handle accordingly
} else {
throw error;
}
}

Use Savepoints for Partial Rollbacks

await db.transact(async (tx) => {
// Critical operation - must succeed
await tx.createOne("order", { ... });

// Optional operation - can fail
try {
await tx.transact(async (inner) => {
await inner.createOne("notification", { ... });
});
} catch {
// Notification failed, but order is still created
console.log("Failed to create notification");
}
});