# Batch operations
Batch operations allow multiple updates to be grouped into a single channel message and applied atomically. It ensures that all operations in a batch either succeed together or are discarded entirely. Batching is essential when multiple related updates to channel objects must be applied as a single atomic unit, for example, when application logic depends on multiple objects being updated simultaneously. Batching ensures that all operations in the batch either succeed or fail together.
## Create a batch context
To batch object operations together, use the `batch()` method on a [PathObject](https://ably.com/docs/liveobjects/concepts/path-object) or [Instance](https://ably.com/docs/liveobjects/concepts/instance). This method accepts a callback function that receives a batch context that allows you to construct operations. Ably publishes these operations together as a single channel message. If an error occurs publishing the batched operations, all operations are discarded, preventing partial updates and ensuring atomicity.
Call `batch()` on a `PathObject` to group operations on that path:
```javascript
const myObject = await channel.object.get();
// Batch multiple operations on the channel object
await myObject.batch((ctx) => {
ctx.set('foo', 'bar');
ctx.set('baz', 42);
ctx.remove('oldKey');
});
// Batch operations on a nested path
await myObject.get('settings').batch((ctx) => {
ctx.set('theme', 'dark');
ctx.set('fontSize', 14);
ctx.set('notifications', true);
});
// Batch operations on a counter
await myObject.get('visits').batch((ctx) => {
ctx.increment(5);
ctx.increment(3);
ctx.decrement(2);
});
```
You can only call `batch()` on an `PathObject` whose path resolves to a `LiveMap` or `LiveCounter` instance:
```javascript
const myObject = await channel.object.get();
try {
// Call batch() on 'username', which resolves to a string value
await myObject.get('username').batch((ctx) => {});
} catch (err) {
// Error 92007: Cannot batch operations on a non-LiveObject at path: username
}
```
You can also call `batch()` on an `Instance` to batch operations on that specific object:
```javascript
const settingsInstance = myObject.get('settings').instance();
if (settingsInstance) {
await settingsInstance.batch((ctx) => {
ctx.set('theme', 'dark');
ctx.set('fontSize', 14);
ctx.remove('oldSetting');
});
}
```
## Create objects in a batch
You can create new objects inside a batch using `LiveMap.create()` and `LiveCounter.create()`. This allows you to atomically create and assign multiple objects in a single operation:
```javascript
const myObject = await channel.object.get();
await myObject.batch((ctx) => {
// Create and assign multiple objects atomically
ctx.set('user', LiveMap.create({
name: 'Alice',
score: LiveCounter.create(0),
settings: LiveMap.create({
theme: 'dark',
notifications: true
})
}));
ctx.set('visits', LiveCounter.create(0));
ctx.set('reactions', LiveMap.create({
likes: 0,
hearts: 0
}));
});
```
Creating objects inside a batch ensures that all objects are created and assigned as a single atomic operation, preventing inconsistent intermediate states.
## Navigate a batch context
Navigate to nested objects using the `get()` method on the batch context. Navigating a batch context with `get()` has the same behaviour as navigating an [`Instance`](https://ably.com/docs/liveobjects/concepts/instance#navigate):
```javascript
await myObject.batch((ctx) => {
// Navigate to nested paths and perform operations
ctx.get('settings')?.set('theme', 'dark');
ctx.get('settings')?.get('preferences')?.set('language', 'en');
// Increment a nested LiveCounter instance
ctx.get('visits')?.increment(5);
});
```
## Cancel a batch
To explicitly cancel a batch before it is applied, throw an error inside the batch function. This prevents any queued operations from being applied:
```javascript
try {
await myObject.batch((ctx) => {
// Cancel the entire batch if a required value is missing
const value = ctx.get('visits')?.value();
if (value === undefined) {
throw new Error('visits key is missing');
}
// Will not be applied if visits is missing
ctx.set('lastSeen', Date.now());
});
} catch (err) {
// Batch operations not applied if visits is missing
}
```
## Understand batch context behavior
The batch context has the same API as [Instance](https://ably.com/docs/liveobjects/concepts/instance), except for `batch()` itself, but all mutation methods are synchronous and queue operations instead of sending them immediately. After the callback completes, all queued operations are sent together in a single channel message.
```javascript
try {
await myObject.batch((ctx) => {
// These operations are synchronous and queued
ctx.get('settings')?.set('theme', 'dark');
ctx.get('visits')?.increment();
});
// All operations published successfully
} catch (err) {
// Failed to publish operations, none of them were published
}
```
Since the batch callback is synchronous, you can read current values inside a batch context without intermediate updates from other clients being applied between reads:
```javascript
await myObject.batch((ctx) => {
ctx.get('settings')?.get('theme')?.value(); // "dark"
// no updates will be applied during execution of the batch callback
ctx.get('settings')?.get('theme')?.value(); // "dark"
});
```
Operations on a batch context are not applied until they are all published, so you cannot read back data written inside the batch context callback:
```javascript
await myObject.batch((ctx) => {
// Get the value at the time batch() was called
ctx.get('settings')?.get('theme')?.value(); // "dark"
// Update the value
ctx.get('settings')?.set('theme', 'light');
// The previous update will not be applied until the batch
// callback completes, so the new value cannot be read back
ctx.get('settings')?.get('theme')?.value(); // "dark"
});
```
The batch context object cannot be used outside the callback function. Attempting to do so results in an error:
```javascript
let context;
await myObject.batch((ctx) => {
context = ctx;
ctx.set('foo', 'bar');
});
// Calling any methods outside the batch callback will throw an error
try {
context.set('baz', 42);
} catch (error) {
// Error: Batch is closed
}
```
When a batch operation is applied, the subscription is notified synchronously and sequentially for each operation included in the batch:
```javascript
// Subscribe to the channel object
myObject.subscribe(({ object, message }) => {
console.log("Update:", message?.operation.action, message?.operation?.mapOp.key);
});
// Perform a batch operation
await myObject.batch((ctx) => {
ctx.set('foo', 'bar');
ctx.set('baz', 42);
ctx.set('qux', 'hello');
});
// Update: map.set foo
// Update: map.set baz
// Update: map.set qux
```