Documentation Index
Fetch the complete documentation index at: https://mintlify.com/kysely-org/kysely/llms.txt
Use this file to discover all available pages before exploring further.
Transactions allow you to execute multiple database operations atomically. If any operation fails, all changes are rolled back. Kysely provides two ways to work with transactions: automatic transactions and controlled transactions.
Automatic Transactions
The transaction() method provides automatic transaction management. The transaction is automatically committed if the callback succeeds, or rolled back if an exception is thrown.
Simple Transaction
This example inserts two rows in a transaction:
const catto = await db.transaction().execute(async (trx) => {
const jennifer = await trx.insertInto('person')
.values({
first_name: 'Jennifer',
last_name: 'Aniston',
age: 40,
})
.returning('id')
.executeTakeFirstOrThrow()
return await trx.insertInto('pet')
.values({
owner_id: jennifer.id,
name: 'Catto',
species: 'cat',
is_favorite: false,
})
.returningAll()
.executeTakeFirst()
})
If an exception is thrown inside the callback:
- The exception is caught
- The transaction is rolled back
- The exception is thrown again
Otherwise the transaction is committed.
Setting Isolation Level
You can configure the transaction isolation level:
await db
.transaction()
.setIsolationLevel('serializable')
.execute(async (trx) => {
await doStuff(trx)
})
async function doStuff(kysely: typeof db) {
// ...
}
Available isolation levels:
'read uncommitted'
'read committed'
'repeatable read'
'serializable'
'snapshot'
Controlled Transactions
Controlled transactions give you manual control over commit and rollback operations, and allow you to work with savepoints.
Manual Commit and Rollback
A controlled transaction allows you to commit and rollback manually:
const trx = await db.startTransaction().execute()
try {
const jennifer = await trx.insertInto('person')
.values({
first_name: 'Jennifer',
last_name: 'Aniston',
age: 40,
})
.returning('id')
.executeTakeFirstOrThrow()
const catto = await trx.insertInto('pet')
.values({
owner_id: jennifer.id,
name: 'Catto',
species: 'cat',
is_favorite: false,
})
.returningAll()
.executeTakeFirstOrThrow()
await trx.commit().execute()
// ...
} catch (error) {
await trx.rollback().execute()
}
Using Savepoints
Savepoints allow you to create checkpoints within a transaction that you can roll back to:
const trx = await db.startTransaction().execute()
try {
const jennifer = await trx
.insertInto('person')
.values({
first_name: 'Jennifer',
last_name: 'Aniston',
age: 40,
})
.returning('id')
.executeTakeFirstOrThrow()
const trxAfterJennifer = await trx.savepoint('after_jennifer').execute()
try {
const catto = await trxAfterJennifer
.insertInto('pet')
.values({
owner_id: jennifer.id,
name: 'Catto',
species: 'cat',
})
.returning('id')
.executeTakeFirstOrThrow()
await trxAfterJennifer
.insertInto('toy')
.values({ name: 'Bone', price: 1.99, pet_id: catto.id })
.execute()
} catch (error) {
await trxAfterJennifer.rollbackToSavepoint('after_jennifer').execute()
}
await trxAfterJennifer.releaseSavepoint('after_jennifer').execute()
await trx.insertInto('audit').values({ action: 'added Jennifer' }).execute()
await trx.commit().execute()
} catch (error) {
await trx.rollback().execute()
}
Setting Isolation Level
You can set the isolation level for controlled transactions:
const trx = await db
.startTransaction()
.setIsolationLevel('serializable')
.execute()
try {
// Your transaction code here
await trx.commit().execute()
} catch (error) {
await trx.rollback().execute()
}
Connection Pooling
The connection() method provides a Kysely instance bound to a single database connection:
await db
.connection()
.execute(async (db) => {
// `db` is an instance of `Kysely` that's bound to a single
// database connection. All queries executed through `db` use
// the same connection.
await doStuff(db)
})
async function doStuff(kysely: typeof db) {
// ...
}
This is useful when you need to ensure multiple queries use the same connection without a transaction.
Transaction Object
The transaction object passed to the callback is of type Transaction<DB>, which extends Kysely<DB>. This means you can use all the same query building methods:
const result = await db.transaction().execute(async (trx) => {
// All Kysely methods are available
await trx.selectFrom('person').selectAll().execute()
await trx.insertInto('person').values({ ... }).execute()
await trx.updateTable('person').set({ ... }).where(...).execute()
await trx.deleteFrom('person').where(...).execute()
// Schema operations
await trx.schema.createTable('temp').addColumn(...).execute()
return someResult
})
Nested Transactions
Kysely doesn’t support true nested transactions, but you can use savepoints to achieve similar functionality:
await db.transaction().execute(async (trx) => {
// First operation
await trx.insertInto('person').values({ ... }).execute()
// Create a savepoint for "nested" transaction
const innerTrx = await trx.savepoint('inner').execute()
try {
await innerTrx.insertInto('pet').values({ ... }).execute()
await innerTrx.releaseSavepoint('inner').execute()
} catch (error) {
await innerTrx.rollbackToSavepoint('inner').execute()
}
// Continue with outer transaction
await trx.insertInto('toy').values({ ... }).execute()
})
Transaction Lifecycle
Once a controlled transaction is committed or rolled back, it cannot be used anymore. All queries will throw an error:
const trx = await db.startTransaction().execute()
await trx.commit().execute()
// This will throw an error!
try {
await trx.selectFrom('person').selectAll().execute()
} catch (error) {
console.error('Cannot use transaction after commit')
}
Best Practices
Keep Transactions Short
Keep transactions as short as possible to minimize lock contention:
// Good: Short transaction
await db.transaction().execute(async (trx) => {
await trx.insertInto('person').values(person).execute()
await trx.insertInto('pet').values(pet).execute()
})
// Bad: Long transaction with non-DB operations
await db.transaction().execute(async (trx) => {
await trx.insertInto('person').values(person).execute()
await sendEmail(person.email) // Don't do this in a transaction!
await trx.insertInto('pet').values(pet).execute()
})
Use Appropriate Isolation Levels
Choose the lowest isolation level that meets your requirements:
// For most cases, read committed is sufficient
await db.transaction()
.setIsolationLevel('read committed')
.execute(async (trx) => { ... })
// Use serializable only when necessary
await db.transaction()
.setIsolationLevel('serializable')
.execute(async (trx) => { ... })
Handle Errors Properly
Always handle errors in controlled transactions:
const trx = await db.startTransaction().execute()
try {
// Your operations
await trx.commit().execute()
} catch (error) {
await trx.rollback().execute()
throw error // Re-throw after rollback
}
API Reference
Automatic Transactions
db.transaction() - Create a transaction builder
.setIsolationLevel(level) - Set isolation level
.execute(callback) - Execute the transaction
Controlled Transactions
db.startTransaction() - Create a controlled transaction builder
.setIsolationLevel(level) - Set isolation level
.execute() - Start the transaction
trx.commit() - Commit the transaction
trx.rollback() - Rollback the transaction
trx.savepoint(name) - Create a savepoint
trx.releaseSavepoint(name) - Release a savepoint
trx.rollbackToSavepoint(name) - Rollback to a savepoint
Connection Pooling
db.connection() - Create a connection builder
.execute(callback) - Execute with a single connection