SurrealTransaction

The SurrealTransaction class provides transaction support for executing multiple queries atomically. When all desired queries have been executed, call commit() to apply the changes to the database, or cancel() to discard them.

Transactions are created using the beginTransaction() method on a SurrealSession instance.

Extends: SurrealQueryable

Source: api/transaction.ts

Transactions ensure that a group of operations are executed atomically - either all succeed or all fail. This is essential for maintaining data consistency when performing related operations.

const txn = await db.beginTransaction();

try {
// All operations succeed or fail together
await txn.create(recordId1).content(data1);
await txn.create(recordId2).content(data2);
await txn.commit(); // Apply all changes
} catch (error) {
await txn.cancel(); // Discard all changes
}

The constructor is not called directly. Use SurrealSession.beginTransaction() to create transactions.

Commit the transaction to the database, applying all changes made within the transaction scope.

After committing, the transaction cannot be used again.

Method Syntax

txn.commit()

Promise<void> - Resolves when the transaction is committed

const txn = await db.beginTransaction();

await txn.create(new RecordId('users', 'alice'))
.content({ name: 'Alice', email: 'alice@example.com' });

await txn.create(new RecordId('users', 'bob'))
.content({ name: 'Bob', email: 'bob@example.com' });

// Commit both creates atomically
await txn.commit();
console.log('Transaction committed successfully');

Cancel and discard all changes made in the transaction.

After canceling, the transaction cannot be used again.

Method Syntax

txn.cancel()

Promise<void> - Resolves when the transaction is canceled

const txn = await db.beginTransaction();

try {
await txn.create(new RecordId('users', 'alice'))
.content({ name: 'Alice' });

// Something goes wrong
throw new Error('Validation failed');
} catch (error) {
// Discard all changes
await txn.cancel();
console.log('Transaction cancelled:', error.message);
}

As SurrealTransaction extends SurrealQueryable, it inherits all query execution methods. All queries executed on a transaction instance are part of the transaction scope:

All of these operations are executed within the transaction context and will be committed or canceled together.

import { Surreal, RecordId } from 'surrealdb';

const db = new Surreal();
await db.connect('ws://localhost:8000');
await db.use({ namespace: 'test', database: 'test' });

// Start a transaction
const txn = await db.beginTransaction();

try {
// Create a user
const user = await txn.create(new RecordId('users', 'john'))
.content({
name: 'John Doe',
email: 'john@example.com',
balance: 1000
});

// Create a purchase
const purchase = await txn.create(new RecordId('purchases', 'purchase1'))
.content({
user: new RecordId('users', 'john'),
amount: 100,
item: 'Widget'
});

// Update user balance
await txn.update(new RecordId('users', 'john'))
.merge({ balance: 900 });

// Commit all changes atomically
await txn.commit();
console.log('Purchase completed successfully');
} catch (error) {
// If anything fails, cancel the transaction
await txn.cancel();
console.error('Purchase failed:', error);
}
async function transferMoney(
db: Surreal,
fromUser: string,
toUser: string,
amount: number
) {
const txn = await db.beginTransaction();

try {
// Get current balances
const from = await txn.select(new RecordId('users', fromUser));
const to = await txn.select(new RecordId('users', toUser));

if (!from || !to) {
throw new Error('User not found');
}

if (from.balance < amount) {
throw new Error('Insufficient funds');
}

// Update both balances
await txn.update(new RecordId('users', fromUser))
.merge({ balance: from.balance - amount });

await txn.update(new RecordId('users', toUser))
.merge({ balance: to.balance + amount });

// Create transaction record
await txn.create(new RecordId('transactions', crypto.randomUUID()))
.content({
from: new RecordId('users', fromUser),
to: new RecordId('users', toUser),
amount,
timestamp: new Date()
});

// Commit all changes
await txn.commit();
console.log(`Transferred ${amount} from ${fromUser} to ${toUser}`);
return true;
} catch (error) {
await txn.cancel();
console.error('Transfer failed:', error);
return false;
}
}

// Use the function
await transferMoney(db, 'alice', 'bob', 50);
async function createUserWithFollows(
db: Surreal,
userData: { name: string; email: string },
followUserIds: string[]
) {
const txn = await db.beginTransaction();

try {
// Create the user
const userId = crypto.randomUUID();
const user = await txn.create(new RecordId('users', userId))
.content(userData);

// Create follow relationships
for (const followId of followUserIds) {
await txn.relate(
new RecordId('users', userId),
new Table('follows'),
new RecordId('users', followId),
{ followedAt: new Date() }
);
}

// Create initial activity log
await txn.create(new RecordId('activity', crypto.randomUUID()))
.content({
user: new RecordId('users', userId),
action: 'user_created',
timestamp: new Date()
});

// Commit everything
await txn.commit();
console.log('User and relationships created successfully');
return user;
} catch (error) {
await txn.cancel();
console.error('Failed to create user:', error);
throw error;
}
}
async function atomicBulkOperation(db: Surreal, records: any[]) {
const txn = await db.beginTransaction();
const results: any[] = [];
const errors: any[] = [];

try {
for (const record of records) {
try {
const result = await txn.create(new Table('items'))
.content(record);
results.push(result);
} catch (error) {
errors.push({ record, error });
}
}

// Only commit if all succeeded
if (errors.length === 0) {
await txn.commit();
console.log(`Successfully created ${results.length} records`);
return { success: true, results };
} else {
await txn.cancel();
console.log(`Failed with ${errors.length} errors, rolled back`);
return { success: false, errors };
}
} catch (error) {
await txn.cancel();
console.error('Transaction failed:', error);
return { success: false, errors: [error] };
}
}

Always use try-catch blocks to ensure transactions are properly canceled on errors:

const txn = await db.beginTransaction();
try {
// Operations
await txn.commit();
} catch (error) {
await txn.cancel(); // Important!
throw error;
}

Execute transactions quickly to avoid locking resources:

// Good: Short transaction
const txn = await db.beginTransaction();
await txn.update(recordId).merge(data);
await txn.commit();

// Avoid: Long-running operations in transactions
const txn = await db.beginTransaction();
await expensiveExternalApiCall(); // Bad!
await txn.update(recordId).merge(data);
await txn.commit();

Once a transaction is committed or canceled, create a new one for subsequent operations:

const txn1 = await db.beginTransaction();
await txn1.create(record1);
await txn1.commit();

// Create a new transaction for next operation
const txn2 = await db.beginTransaction();
await txn2.create(record2);
await txn2.commit();

Perform validation before starting a transaction when possible:

// Validate first
if (!isValidEmail(email)) {
throw new Error('Invalid email');
}

// Then transact
const txn = await db.beginTransaction();
// ... operations
await txn.commit();

Was this page helpful?