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

  1. Treat WatchError as a transient, retryable condition: catch it and replay the entire WATCH + MULTI/EXEC sequence on the (now current) master.
  2. Keep WATCH/MULTI/EXEC transactions short to shrink the window in which a Sentinel switch can invalidate them.
  3. 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

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


AI-assisted analysis of redis/node-redis@bb5beb5657 (2026-08-03). Data as JSON: /data/errors/5411ba00c490aebd.json. Report an issue: GitHub.