Advanced client patterns with @sanity/client
Learn about mutation options, request cancellation, and other advanced patterns when using the Sanity JavaScript client.
This guide covers advanced patterns for working with @sanity/client, including mutation options that control how changes are applied, the retries the client performs on your behalf, and techniques for canceling in-flight requests.
These patterns apply to any mutation method: create(), patch(), delete(), and transaction().commit(). See Creating and updating documents and Creating transactions for the basics.
Mutation options
Control how mutations execute with options for visibility, dry runs, and array key generation. These options apply to any mutation method, not only transactions.
Visibility: sync, async, and deferred
The visibility option controls when mutations become visible to queries:
sync: The mutation completes and indexes before returning. Queries immediately see the changes. This is the default behavior.async: The mutation returns immediately but indexes in the background. Queries may not see changes right away.deferred: The mutation queues for later processing. Use this for non-critical updates or bulk operations.
// Sync: wait for indexing (default)
await client.create(
{_type: 'post', title: 'Hello'},
{visibility: 'sync'}
)
// Async: return immediately, index in background
await client.create(
{_type: 'post', title: 'Hello'},
{visibility: 'async'}
)
// Deferred: queue for later processing
await client.create(
{_type: 'analytics', event: 'page_view'},
{visibility: 'deferred'}
)
// Use async for bulk operations
const transaction = client.transaction()
for (const item of largeDataset) {
transaction.create({_type: 'product', ...item})
}
await transaction.commit({visibility: 'async'})Dry run mode
Test mutations without applying changes using dryRun: true. The API validates the mutation and returns what would happen, but does not modify any documents:
// Test a mutation without applying it
const result = await client.create(
{_type: 'post', title: 'Test Post'},
{dryRun: true}
)
console.log('Would create:', result)
// Document is not actually created
// Test a transaction
const txResult = await client
.transaction()
.create({_type: 'author', name: 'Jane'})
.patch('post-123', (p) => p.set({author: 'jane-id'}))
.commit({dryRun: true})
console.log('Transaction would execute:', txResult.results)
// No changes are appliedAuto-generate array keys
By default, the client automatically generates _key values for array items. Disable this with autoGenerateArrayKeys: false if you want to provide your own keys:
// Default: keys are auto-generated
await client.create({
_type: 'post',
title: 'Hello',
tags: [
{_type: 'tag', name: 'javascript'},
{_type: 'tag', name: 'sanity'}
]
})
// Each tag gets a unique _key automatically
// Provide your own keys
await client.create(
{
_type: 'post',
title: 'Hello',
tags: [
{_key: 'js-tag', _type: 'tag', name: 'javascript'},
{_key: 'sanity-tag', _type: 'tag', name: 'sanity'}
]
},
{autoGenerateArrayKeys: false}
)Automatic retries
@sanity/client retries some failed requests on its own. Retries are on by default, so you don't need to add your own backoff around a query.
The client retries a request when the response status is 429 Too Many Requests, 502 Bad Gateway, or 503 Service Unavailable, and the request is one of these:
- A
GETorHEADrequest, such asgetDocument(). - A query to the
/data/queryendpoint, including a query long enough that the client sends it as aPOST.
The client also retries DNS ENOTFOUND failures on idempotent requests, meaning GET and HEAD, using the same backoff. A hostname that never resolves, such as a mistyped project ID, takes the full retry sequence to fail rather than failing at once. Set maxRetries: 0 to fail immediately instead. This applies to @sanity/client v8 and later.
Mutations are not retried
Retries cover reads only. create(), patch(), delete(), transaction().commit(), document and release actions, and asset uploads all go to endpoints the client does not retry, so a 429 on any of them reaches your code immediately.
Back those requests off yourself, or send them through a rate-limited queue. Importing data shows a queue that stays under the mutation rate limit.
A request whose body is a stream is never retried either, because the body has already been read.
Retry defaults
Two ClientConfig options control retries:
maxRetries: how many times to retry a failed request. Defaults to5. Set it to0to turn retries off.retryDelay: a function that receives the attempt number, starting at0, and returns how long to wait in milliseconds. Defaults to exponential backoff with jitter:100 * 2 ** attemptNumberplus a random 0 to 100 milliseconds.
With the defaults, the waits are roughly 100, 200, 400, 800, and 1,600 milliseconds, each plus up to 100 milliseconds of jitter. A request that exhausts all five retries fails after about 3.1 to 3.7 seconds.
import {createClient} from '@sanity/client'
// Retry more patiently: eight attempts, backing off from 250ms
export const client = createClient({
projectId: 'YOUR_PROJECT_ID',
dataset: 'production',
apiVersion: '2025-02-19',
useCdn: true,
maxRetries: 8,
retryDelay: (attemptNumber) => 250 * 2 ** attemptNumber + Math.random() * 100,
})
// Turn retries off entirely
export const noRetryClient = createClient({
projectId: 'YOUR_PROJECT_ID',
dataset: 'production',
apiVersion: '2025-02-19',
maxRetries: 0,
})Canceling requests
Cancel in-flight requests using AbortController or by unsubscribing from Observables. This is useful for cleaning up requests when components unmount or when user actions make a request obsolete.
Using AbortController
Pass an AbortSignal to any client method to enable cancellation:
const controller = new AbortController()
const {signal} = controller
// Start a mutation
const mutationPromise = client.create(
{_type: 'post', title: 'Hello'},
{signal}
)
// Cancel the mutation after 1 second
setTimeout(() => {
controller.abort()
console.log('Mutation canceled')
}, 1000)
try {
await mutationPromise
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.log('Request was aborted')
}
}
// Use with transactions
const txController = new AbortController()
const transaction = client
.transaction()
.create({_type: 'post', title: 'Post 1'})
.create({_type: 'post', title: 'Post 2'})
try {
await transaction.commit({signal: txController.signal})
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.log('Transaction was aborted')
}
}Unsubscribing from Observables
When using the Observable API, unsubscribe to cancel the request:
const subscription = client
.observable
.create({_type: 'post', title: 'Hello'})
.subscribe({
next: (result) => console.log('Created:', result),
error: (error) => console.error('Error:', error),
complete: () => console.log('Complete')
})
// Cancel by unsubscribing
setTimeout(() => {
subscription.unsubscribe()
console.log('Unsubscribed from mutation')
}, 1000)
// Use with transactions
const txSubscription = client.observable
.transaction()
.create({_type: 'post', title: 'Post 1'})
.create({_type: 'post', title: 'Post 2'})
.commit()
.subscribe({
next: (result) => console.log('Transaction result:', result),
error: (error) => console.error('Transaction error:', error)
})
// Unsubscribe to cancel
txSubscription.unsubscribe()React cleanup example
This example shows how to cancel requests when a React component unmounts:
import {useEffect, useState} from 'react'
import {type SanityDocument} from '@sanity/client'
import {client} from './sanityClient'
function useCreatePost(postData: Record<string, unknown>) {
const [result, setResult] = useState<SanityDocument | null>(null)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
const controller = new AbortController()
client
.create(postData, {signal: controller.signal})
.then(setResult)
.catch((err: Error) => {
if (err.name !== 'AbortError') {
setError(err)
}
})
// Cleanup: abort request if component unmounts
return () => controller.abort()
}, [postData])
return {result, error}
}Reference documentation
For the complete set of mutation options, error types, and client configuration, see the reference documentation for SanityClient and ClientConfig.