redis/node-redis · error · WatchError
One (or more) of the watched keys has been changed
Error message
One (or more) of the watched keys has been changed
What it means
Thrown from _executeMulti (client/index.ts:1928) when #dirtyWatch is set. The client-side flag is set via setDirtyWatch(), which the Sentinel client calls ('sentinel config changed in middle of a WATCH Transaction') when a Sentinel master switch happens during a WATCH transaction. Rather than risk executing EXEC against a new master that never saw the WATCH, the client aborts the transaction locally and never sends EXEC. It surfaces as a WatchError carrying the dirty-watch reason.
Source
Thrown at packages/client/lib/client/index.ts:1928
* @internal
*/
async _executeMulti(
commands: Array<RedisMultiQueuedCommand>,
selectedDB?: number
) {
assertNoHimportSessionCommands(commands);
const dirtyWatch = this._self.#dirtyWatch;
this._self.#dirtyWatch = undefined;
const watchEpoch = this._self.#watchEpoch;
this._self.#watchEpoch = undefined;
if (!this._self.#socket.isOpen) {
throw new ClientClosedError();
}
if (dirtyWatch) {
throw new WatchError(dirtyWatch);
}
if (watchEpoch && watchEpoch !== this._self.socketEpoch) {
throw new WatchError('Client reconnected after WATCH');
}
const batchSize = commands.length;
return trace(CHANNELS.TRACE_BATCH,
async () => {
const typeMapping = this._commandOptions?.typeMapping;
const chainId = Symbol('MULTI Chain');
const promises: Array<Promise<unknown>> = [
this._self.#queue.addCommand(['MULTI'], { chainId }),
];
for (const { args } of commands) {
promises.push(View on GitHub (pinned to bb5beb5657)
Solutions
- Treat WatchError as a transient, retryable condition: catch it and replay the entire WATCH + MULTI/EXEC sequence on the (now current) master.
- Keep WATCH/MULTI/EXEC transactions short to shrink the window in which a Sentinel switch can invalidate them.
- After a catch, re-acquire the leased/current master from Sentinel before retrying, since the old connection's master is gone.
Example fix
// before
await client.watch('k');
const res = await client.multi().get('k').set('k', 'v').exec(); // rejects with WatchError on master switch
// after
async function txn(client) {
for (let attempt = 0; attempt < 3; attempt++) {
await client.watch('k');
try {
return await client.multi().get('k').set('k', 'v').exec();
} catch (e) {
if (e.name === 'WatchError' && attempt < 2) continue;
throw e;
}
}
} Defensive patterns
Strategy: retry
Validate before calling
if (client.isDirtyWatch) {
// a topology change has already invalidated WATCH; do not EXEC
await client.unwatch();
} Type guard
import { WatchError } from '@redis/client';
function isWatchError(e: unknown): e is WatchError {
return e instanceof Error && (e instanceof WatchError || e.constructor.name === 'WatchError');
} Try / catch
try {
await client.multi().set('k', 'v').exec();
} catch (e) {
if (isWatchError(e)) { /* re-acquire master from Sentinel, replay WATCH+MULTI */ }
else throw e;
} Prevention
- Keep WATCH/MULTI/EXEC transactions short in Sentinel deployments.
- Check client.isDirtyWatch before queuing commands and UNWATCH if true.
- Always replay the full WATCH+transaction on WatchError; never assume EXEC ran.
When it happens
Trigger: Using a Sentinel-managed client: call client.WATCH(key), then while a multi()/EXEC is in flight a Sentinel master failover/switch occurs, marking the client dirty. The next multi().exec() rejects before contacting the server.
Common situations: Sentinel deployments under failover; long-running MULTI/EXEC transactions that span a master switch; testing against a Sentinel topology that flips masters frequently.
Related errors
- Client reconnected after WATCH
- Scan iteration was interrupted by a Sentinel master change
- no valid master node
- Could not find shard
- Request policy ${requestPolicy} produced no target nodes
AI-assisted analysis of redis/node-redis@bb5beb5657 (2026-08-03).
Data as JSON: /data/errors/5411ba00c490aebd.json.
Report an issue: GitHub.