# Transactions (/docs/orm/fundamentals/transactions) > For the complete Prisma documentation index, see [llms.txt](https://www.prisma.io/docs/llms.txt). A markdown version of any docs page is available by appending `.md` to its URL. Run several writes so they all succeed or all fail together with db.transaction(). Location: ORM > Fundamentals > Transactions Run several writes as one unit with `db.transaction(...)`: they all commit together, or they all roll back. Prisma ORM 8 runs on PostgreSQL, SQLite, and MongoDB, but there is no MySQL support. The examples on this page are PostgreSQL; `db.transaction(...)` works the same way on SQLite, and MongoDB is covered in [Transactions on MongoDB](#transactions-on-mongodb). ## Run writes in a transaction [#run-writes-in-a-transaction] Use a transaction whenever one business operation spans more than one write: creating a user with their first records, moving a value between two rows, or deleting a parent after its children. Pass a callback to `db.transaction(...)`, and inside it, query through `tx` instead of `db`. `tx.orm` is the model API, what `prisma.user` was in Prisma ORM 7; `tx.sql` is the SQL query builder, and both work the same as `db.orm` and `db.sql`. Every query you make on `tx` runs inside the transaction, and `db.transaction(...)` returns whatever your callback returns: ```typescript import { db } from "./prisma/db"; const result = await db.transaction(async (tx) => { const user = await tx.orm.public.User.create({ email: "jane@prisma.io", name: "Jane" }); const post = await tx.orm.public.Post.create({ title: "Hello", published: false, authorId: user.id }); return { userId: user.id, postId: post.id }; // both records exist once this returns }); ``` `db` is the client you create once in `src/prisma/db.ts`, and `prisma orm init` writes that file for you. It writes the file once; it is your code, so edit it freely. Under `tx.orm.public`, the property name is the model name, so `User` and `Post` are spelled exactly as you spelled them in your contract. `public` is the PostgreSQL schema, the namespace your tables live in, which is `public` unless you set one. `create(...)` takes the fields of the record directly, with no `data:` wrapper: pass a field if it is required and has no default, while everything else is optional, and [Write data](https://www.prisma.io/docs/orm/fundamentals/writing-data) has the full rules. You don't need a transaction for a single write; every mutation is already atomic on its own. ### Read inside a transaction [#read-inside-a-transaction] Reads work the same way as writes: query through `tx.orm` and you see the rows the transaction has already written. `.first()` returns `null` when nothing matches. ```typescript await db.transaction(async (tx) => { await tx.orm.public.User.create({ email: "jane@prisma.io", name: "Jane" }); const jane = await tx.orm.public.User.where({ email: "jane@prisma.io" }).first(); // jane is the record created a line earlier, before the transaction commits }); ``` ### Pass tx to your helper functions [#pass-tx-to-your-helper-functions] A helper that runs inside the caller's transaction takes `tx` as a parameter: ```typescript import { db, type Tx } from "./prisma/db"; async function createWelcomePost(tx: Tx, authorId: string) { return tx.orm.public.Post.create({ title: "Welcome", published: false, authorId }); } await db.transaction(async (tx) => { const user = await tx.orm.public.User.create({ email: "jane@prisma.io", name: "Jane" }); await createWelcomePost(tx, user.id); }); ``` Prisma ORM does not export a type for `tx`, so add this line to `src/prisma/db.ts`: `export type Tx = Parameters[0]>[0];`, then import `Tx` wherever a helper needs it. Do not open a second transaction inside the helper, because calling `db.transaction(...)` again inside a callback does not nest: the inner call is a separate transaction that commits or rolls back on its own, and the outer one cannot roll it back. Pass `tx` down instead. ### Options and isolation level [#options-and-isolation-level] `db.transaction(...)` takes the callback and nothing else. There are no `isolationLevel`, `timeout`, or `maxWait` options; see [what is not available yet](https://www.prisma.io/docs/orm/coming-from-prisma-orm-7#not-available-yet). Every transaction runs at the database's default isolation level, `READ COMMITTED` on PostgreSQL, and you cannot ask for another level yet. If you need one, set it yourself as the first statement in the callback: ```typescript await db.transaction(async (tx) => { await tx.execute(db.raw.sql`SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`.affectedCount().build()); // your writes }); ``` There is no transaction timeout either, so a transaction stays open for as long as your callback runs. If you need a limit, set PostgreSQL's `idle_in_transaction_session_timeout` on the connection (`transaction_timeout` on PostgreSQL 17 and later). ## Roll back on errors [#roll-back-on-errors] The transaction commits when the callback returns and rolls back when it throws, so nothing inside the callback survives an error: ```typescript try { await db.transaction(async (tx) => { await tx.orm.public.User.create({ email: "ghost@prisma.io", name: "Ghost" }); throw new Error("boom"); }); } catch { // The user record was rolled back and does not exist } ``` ### Write conflicts [#write-conflicts] When PostgreSQL cannot commit your transaction because another transaction touched the same rows, it aborts yours. Errors from the database arrive with a `sqlState` property holding the PostgreSQL error code (`40001` for a serialization failure, when two transactions changed the same rows and one had to give way; `40P01` for a deadlock). When the conflict surfaces at commit, the error you catch has the code `RUNTIME.TRANSACTION_COMMIT_FAILED` and the database error sits on its `cause`. There is no exported error class to check with `instanceof`, so test for the property, on the error and on its `cause`. Prisma ORM does not retry write conflicts for you, so write the retry yourself around the `db.transaction(...)` call: ```typescript async function withRetry(run: () => Promise, attempts = 3): Promise { for (let attempt = 1; ; attempt++) { try { return await run(); } catch (error) { const source = error instanceof Error && error.cause ? error.cause : error; const code = typeof source === "object" && source && "sqlState" in source ? source.sqlState : null; if ((code !== "40001" && code !== "40P01") || attempt === attempts) throw error; } } } await withRetry(() => db.transaction(async (tx) => { /* your writes */ })); ``` ## Use the SQL builder in a transaction [#use-the-sql-builder-in-a-transaction] The [SQL query builder](https://www.prisma.io/docs/orm/fundamentals/advanced-queries) works the same way inside a transaction. `tx.sql` has the methods `db.sql` has, and you run the finished query with `tx.execute(...)`: ```typescript const cutoff = Temporal.Instant.from("2026-01-01T00:00:00Z"); await db.transaction(async (tx) => { const query = tx.sql.public.Post .update({ published: false }) .where((f, fns) => fns.lt(f.createdAt, cutoff)) .build(); await tx.execute(query); }); ``` `tx.orm` takes the model name and `tx.sql` takes the table name, which is the model's `@@map` value, or the model name exactly as written when the model has no `@@map`. So the model `Post` is `tx.sql.public.Post`, and a model with `@@map("post")` is `tx.sql.public.post`. The other difference from `tx.orm` is that you chain clauses and then call `.build()` to finish the query: `.build()` returns the query without running it, and `tx.execute(...)` runs it and returns `{ affectedRows }`, the number of rows it changed. The `.where(...)` callback receives two arguments: `f` holds the columns, and `fns` holds the operators (`lt` is less-than). [Advanced queries](https://www.prisma.io/docs/orm/fundamentals/advanced-queries#build-a-query-and-run-it) covers both. To read rows, build a `SELECT` on `tx.sql` the same way and run it with `tx.query(...)`, which resolves to an array of rows: ```typescript title="src/read-posts.ts" await db.transaction(async (tx) => { const query = tx.sql.public.Post .select("id", "title") .where((f, fns) => fns.eq(f.published, false)) .limit(10) .build(); const drafts = await tx.query(query); }); ``` ## Transactions on MongoDB [#transactions-on-mongodb] Prisma ORM does not support MongoDB transactions yet: there is no `db.transaction(...)` on the MongoDB client. To run a multi-document transaction today, use the MongoDB driver directly: share one `MongoClient` between Prisma ORM and your code, and group the writes in a driver session. MongoDB only runs transactions on a replica set, and a standalone server rejects them. To turn a standalone server into a single-member replica set, follow the MongoDB guide on [converting a standalone to a replica set](https://www.mongodb.com/docs/manual/tutorial/convert-standalone-to-replica-set/). Install `@prisma/orm-mongo`, which a MongoDB project already has, together with version 7 of the `mongodb` driver: #### bun ```bash bun add @prisma/orm-mongo mongodb@7 ``` #### pnpm ```bash pnpm add @prisma/orm-mongo mongodb@7 ``` #### yarn ```bash yarn add @prisma/orm-mongo mongodb@7 ``` #### npm ```bash npm install @prisma/orm-mongo mongodb@7 ``` `prisma orm init` writes `src/prisma/db.ts` with a `url` option. Replace that option with a `MongoClient` you create and export yourself, so your driver code and Prisma ORM use the same connection: Run `npx prisma contract emit` once first; `db.ts` imports the two files it writes. ```typescript title="src/prisma/db.ts" import "dotenv/config"; import mongo from "@prisma/orm-mongo/runtime"; import { MongoClient } from "mongodb"; import type { Contract } from "./contract.d"; // the two files `prisma contract emit` writes import contractJson from "./contract.json" with { type: "json" }; export const client = new MongoClient(process.env["DATABASE_URL"]!); export const db = mongo({ contractJson, mongoClient: client, dbName: "app", }); ``` Keep the `contractJson` import and the `Contract` type, and run `prisma contract emit` after every change to your contract, on PostgreSQL and on MongoDB alike. `dbName` is now the only thing that picks the database, so set it to the database your driver code reads, and `client.db("app")` and Prisma ORM stay on the same one. An empty `dbName` is an error. ```typescript import { client, db } from "./prisma/db"; const session = client.startSession(); try { await session.withTransaction(async () => { const database = client.db("app"); const users = database.collection("users"); const user = await users.insertOne({ email: "jane@prisma.io", name: "Jane" }, { session }); const post = { title: "Hello", published: false, authorId: user.insertedId }; await database.collection("posts").insertOne(post, { session }); }); } finally { await session.endSession(); } // Prisma ORM reads see the committed result const jane = await db.orm.users.where({ email: "jane@prisma.io" }).first(); ``` On MongoDB there is no schema segment in the path, and the property name is the collection name: the model's `@@map` value, or the model name exactly as written when the model has no `@@map`. The example above assumes a `User` model mapped to the `users` collection, like the one below, so Prisma ORM reads it as `db.orm.users` and the driver code as `database.collection("users")`. Without the `@@map` line it would be `db.orm.User`. ```prisma title="src/prisma/contract.prisma (excerpt)" model User { id ObjectId @id @map("_id") email String @unique name String? @@map("users") } ``` The `id` field has no `@default` because you never pass it: when you insert a document without an `_id`, MongoDB assigns one, and `insertOne` returns it as `insertedId`, which the example uses as the post's `authorId`. Writes you make through the driver skip the type-checking that Prisma ORM queries get, so put each driver transaction in its own small function, and write everything around it as Prisma ORM queries. ## Coming from Prisma ORM 7 [#coming-from-prisma-orm-7] Prisma ORM 8 has no `$transaction`, so both of the Prisma ORM 7 forms become `db.transaction(async (tx) => ...)`. The interactive form maps one for one: rename the method and query through `tx`. ```diff - const result = await prisma.$transaction(async (prisma) => { - const user = await prisma.user.create({ data: { email, name } }); - return prisma.post.create({ data: { title, authorId: user.id } }); - }); + const result = await db.transaction(async (tx) => { + const user = await tx.orm.public.User.create({ email, name }); + // published is required and has no default, so you pass it + return tx.orm.public.Post.create({ title, published: false, authorId: user.id }); + }); ``` The array form has no replacement of its own, so write the queries out in the callback, one after another: ```diff - const [user, post] = await prisma.$transaction([ - prisma.user.create({ data: { email, name } }), - prisma.post.create({ data: { title, authorId } }), - ]); + const { user, post } = await db.transaction(async (tx) => { + const user = await tx.orm.public.User.create({ email, name }); + const post = await tx.orm.public.Post.create({ title, published: false, authorId: user.id }); + return { user, post }; + }); ``` You get the same atomicity, and you also get what the array form never allowed: one query's result (here `user.id`) can feed the next query in the same transaction. ## Common mistakes [#common-mistakes] ### Side effects inside the callback [#side-effects-inside-the-callback] Sending an email or queueing a job inside the callback looks natural, because it is right next to the write it belongs to: ```typescript await db.transaction(async (tx) => { const user = await tx.orm.public.User.create({ email, name }); await sendWelcomeEmail(user.email); // runs even if the transaction rolls back }); ``` Database writes roll back; emails don't. If a later statement throws, the record disappears but the email was already sent. Return what you need from the callback, and run the side effect after the transaction has committed: ```typescript const user = await db.transaction(async (tx) => { return tx.orm.public.User.create({ email, name }); }); await sendWelcomeEmail(user.email); ``` Now the email can only go out for a user that actually exists. ### Querying through db instead of tx [#querying-through-db-instead-of-tx] If you open a transaction but keep writing `db.orm` inside the callback: ```typescript await db.transaction(async (tx) => { await db.orm.public.User.create({ email, name }); // outside the transaction }); ``` Queries on `db` run on their own connection, outside the open transaction, so they commit immediately and won't roll back with the rest of the callback. Use `tx` for every query inside the callback: `tx.orm` for models, `tx.sql` and `tx.execute` for the SQL query builder. ## Prompt your coding agent [#prompt-your-coding-agent] Projects created with `npm create prisma@latest` include the [Prisma ORM skills](https://www.prisma.io/docs/ai/tools/skills#available-skills-for-prisma-8), instruction files for coding agents, and in an existing project you run `npx prisma skills sync` to add them. The `prisma-8` skill covers transactions, so try prompts that map to each section: * "Using the prisma-8 skill, wrap this signup flow (create user, create welcome post) in a db.transaction so both writes commit together." * "Check this transaction callback for queries that use db instead of tx, and move the email send after the commit." * "Refactor these two service functions so the helper takes tx as a parameter instead of opening its own transaction." * "This project is on MongoDB. Show me the driver-session pattern for an atomic two-collection write with a shared MongoClient." ## Next [#next] * [Write data](https://www.prisma.io/docs/orm/fundamentals/writing-data): the single-record and bulk mutations you group in a transaction. * [Use advanced queries](https://www.prisma.io/docs/orm/fundamentals/advanced-queries) to run SQL query builder statements inside or outside transactions. ## Related pages - [`Advanced queries`](https://www.prisma.io/docs/orm/fundamentals/advanced-queries): Use the SQL query builder on PostgreSQL and the pipeline builder on MongoDB for queries the ORM API can't express. - [`Reading data`](https://www.prisma.io/docs/orm/fundamentals/reading-data): Fetch one record or many with Prisma ORM, then filter, select, sort, paginate, and iterate the results. - [`Relations and joins`](https://www.prisma.io/docs/orm/fundamentals/relations-and-joins): Read related records in one query with .include(), and understand how one-to-one, one-to-many, and many-to-many relationships work. - [`Writing data`](https://www.prisma.io/docs/orm/fundamentals/writing-data): Create, update, delete, and upsert records with Prisma ORM, one at a time or in bulk.