---
id: "packages/active-state/transactions"
title: "Batches and transactions"
description: "Group notifications with batch, or make several writes all-or-nothing with transaction."
url: "/packages/active-state/transactions"
locale: "en"
revision: "cb65a9f7d06173e9"
---

# Batches and transactions

**Example: An all-or-nothing transfer**

Client: Runs entirely in your browser. Source: `examples/state/transfer-transaction.tsx`.

```tsx
// apps/docs/examples/state/transfer-transaction.tsx
'use client';

import {useState} from 'react';

import {
  Derived,
  Store,
  transaction,
  useStore,
} from '@workspace/active-state/react';
import {Button} from '@workspace/active-ui/components/primitives/button';

const savings = new Store(100);
const current = new Store(20);
const total = new Derived({
  inputs: [savings, current],
  compute: (a, b) => a + b,
});

/** Moves `amount`; throws, and so reverts both writes, when savings run short. */
function transfer(amount: number) {
  transaction(() => {
    current.update((value) => value + amount);
    savings.update((value) => value - amount);
    if (savings.state < 0) throw new Error('Not enough savings');
  });
}

/** A transaction is all-or-nothing: a failed transfer changes neither account. */
export default function TransferTransaction() {
  const [error, setError] = useState<string | null>(null);
  const a = useStore(savings);
  const b = useStore(current);
  const sum = useStore(total);
  const move = (amount: number) => {
    try {
      transfer(amount);
      setError(null);
    } catch (thrown) {
      setError(thrown instanceof Error ? thrown.message : String(thrown));
    }
  };
  return (
    <div className="flex flex-col gap-3">
      <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
        <dt className="text-muted-foreground">Savings</dt>
        <dd data-account="savings">{a}</dd>
        <dt className="text-muted-foreground">Current</dt>
        <dd data-account="current">{b}</dd>
        <dt className="text-muted-foreground">Total</dt>
        <dd>{sum}</dd>
      </dl>
      <div className="flex flex-wrap gap-2">
        <Button variant="outline" size="sm" onClick={() => move(30)}>
          Transfer 30
        </Button>
        <Button variant="outline" size="sm" onClick={() => move(500)}>
          Transfer 500
        </Button>
      </div>
      <p className="min-h-5 text-sm" role="status">
        {error ? `Reverted: ${error}. Neither account changed.` : null}
      </p>
    </div>
  );
}
```

## `batch`: one notification for several writes

```ts
import {batch} from '@workspace/active-state';

batch(() => {
  first.setState('Ada');
  last.setState('Lovelace');
}); // subscribers run once, after both writes
```

Writes inside a batch are readable at once and published together when the outermost batch ends.
**A batch never reverts anything**: if the callback throws, its writes stay, are published, and the
error is rethrown.

## `transaction`: all or nothing

```ts
import {transaction} from '@workspace/active-state';

transaction(() => {
  current.update((value) => value + amount);
  savings.update((value) => value - amount);
  if (savings.state < 0) throw new Error('Not enough savings');
});
```

- Reads inside see the transaction's own writes, for stores and derived values alike.
- When the callback throws, every store and derived value it touched is restored, nothing is
  published, and the first error is rethrown to you.
- A nested `transaction` joins the open one; there are no savepoints.
- Only local state is reverted: a request, a DOM change or a copy in another variable is not.
- Structural calls (subscribing, disposing, creating effects or operations) are refused inside a
  transaction; `invoke()` waits until it commits.

Both are synchronous: a callback that returns a promise is refused with a `TypeError`.
