> ## Documentation Index
> Fetch the complete documentation index at: https://docs.larksh.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Transactions

> Atomic operations with the Lark SDK

Transactions let you update data based on its current value without worrying about conflicts from other clients. They're essential for counters, inventory systems, auctions, and any scenario where a write depends on the existing data.

For a conceptual overview of how transactions work and why you need them, see [Transactions](/platform/transactions).

## Callback-style transactions

The most common pattern. Pass a function that receives the current value and returns the new value:

```typescript theme={null}
import { LarkDatabase } from "@lark-sh/client";

const db = new LarkDatabase("my-project/my-database", { anonymous: true });

const result = await db.ref("counters/pageViews").transaction((currentValue) => {
  return (currentValue ?? 0) + 1;
});

console.log(result.committed); // true if the transaction succeeded
console.log(result.snapshot.val()); // The final committed value
```

### Transaction results

Every `transaction()` call returns a result object:

| Property    | Type           | Description                                                      |
| ----------- | -------------- | ---------------------------------------------------------------- |
| `committed` | `boolean`      | `true` if the transaction wrote data, `false` if it was aborted. |
| `snapshot`  | `DataSnapshot` | The final value at the path after the transaction.               |

### Aborting a transaction

Return `undefined` from your update function to abort without writing:

```typescript theme={null}
const result = await db.ref("inventory/item-1").transaction((current) => {
  if (current === null) {
    return undefined; // Item doesn't exist, abort
  }

  if (current.quantity <= 0) {
    return undefined; // Out of stock, abort
  }

  return {
    ...current,
    quantity: current.quantity - 1,
  };
});

if (!result.committed) {
  console.log("Transaction was aborted");
}
```

### Retry limit

Transactions automatically retry up to **25 times**. If they still can't commit after 25 attempts (due to extremely high contention), the promise rejects with a `max_retries_exceeded` error.

```typescript theme={null}
try {
  await db.ref("hot-counter").transaction((val) => (val ?? 0) + 1);
} catch (error) {
  if (error.code === "max_retries_exceeded") {
    console.error("Too much contention on this path");
  }
}
```

<Note>
  Your update function may be called **multiple times** if there are concurrent writes. Make sure it has no side effects. Don't make network requests, modify external state, or log analytics inside it.
</Note>

## Multi-path transactions

When you need to update multiple paths atomically, either all the changes happen or none of them do.

### Object syntax

The simplest form. Pass an object where keys are paths and values are what to write. Use `null` to delete a path.

```typescript theme={null}
await db.transaction({
  "/players/alice/coins": 50,
  "/players/bob/coins": 150,
  "/trades/latest": { from: "alice", to: "bob", amount: 50 },
});
```

All three writes happen atomically. If any one fails, none of them are applied.

### Array syntax with conditions

For more control, use the array syntax with explicit operations. This lets you add conditions that check the current value before proceeding.

```typescript theme={null}
await db.transaction([
  // Only proceed if Alice has exactly 100 coins
  { op: "condition", path: "/players/alice/coins", value: 100 },
  // Transfer 50 coins
  { op: "set", path: "/players/alice/coins", value: 50 },
  { op: "set", path: "/players/bob/coins", value: 150 },
]);
```

If any condition fails, the entire transaction is rejected and no writes are applied.

You can also pass a snapshot of a complex object as a condition. Internally, the LarkJS library will compute a hash representing the state of this object and pass it as the condition to the server, keeping the transaction efficient (so the entire object isn't sent).

```typescript theme={null}

const snapshot = await db.ref("/game/state").once("value");

await db.transaction([
  { op: "condition", path: "/game/state", value: snapshot },
  { op: "set", path: "/game/state", value: newState },
]);
```

## Examples

### Increment a counter

```typescript theme={null}
await db.ref("stats/totalGames").transaction((current) => {
  return (current ?? 0) + 1;
});
```

### Update a high score (only if higher)

```typescript theme={null}
const newScore = 250;

const result = await db.ref("players/alice/highScore").transaction((current) => {
  if (current !== null && current >= newScore) {
    return undefined; // Current high score is already higher, abort
  }
  return newScore;
});

if (result.committed) {
  console.log("New high score recorded:", result.snapshot.val());
}
```

### Transfer currency between players

```typescript theme={null}
// Read current values first
const aliceSnapshot = await db.ref("players/alice/coins").once("value");
const bobSnapshot = await db.ref("players/bob/coins").once("value");
const aliceCoins = aliceSnapshot.val();
const bobCoins = bobSnapshot.val();

const transferAmount = 50;

// Use a conditional multi-path transaction
await db.transaction([
  // Ensure neither balance changed since we read it
  { op: "condition", path: "/players/alice/coins", value: aliceCoins },
  { op: "condition", path: "/players/bob/coins", value: bobCoins },
  // Apply the transfer
  { op: "set", path: "/players/alice/coins", value: aliceCoins - transferAmount },
  { op: "set", path: "/players/bob/coins", value: bobCoins + transferAmount },
]);
```

### Claim a unique resource

Use a transaction to ensure only one client can claim something:

```typescript theme={null}
const result = await db.ref("game/crown").transaction((current) => {
  if (current !== null) {
    return undefined; // Someone already claimed it
  }
  return { claimedBy: "alice", claimedAt: Date.now() };
});

if (result.committed) {
  console.log("Crown claimed!");
} else {
  console.log("Someone else got it first");
}
```

### Conditional multi-path update

Only claim a reward if it hasn't been claimed yet:

```typescript theme={null}
await db.transaction([
  { op: "condition", path: "/rewards/reward1/claimed", value: false },
  { op: "set", path: "/rewards/reward1/claimed", value: true },
  { op: "set", path: "/rewards/reward1/claimedBy", value: "alice" },
]);
```

<Warning>
  Avoid running transactions on paths with very high write contention from many clients simultaneously. If you're hitting the retry limit frequently, consider restructuring your data to reduce contention. For example, you could shard counters across multiple paths.
</Warning>
