redis/node-redis · warning · 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 in _executeMulti when EXEC returns null. Redis returns null from EXEC when any WATCHed key was modified between WATCH and EXEC, meaning the transaction was aborted and no commands ran. This is normal optimistic-locking behavior, surfaced as a WatchError so callers can detect it and retry the read-modify-write cycle.

Solutions

  1. Treat WatchError as a retry signal: re-read the current value, re-issue WATCH, and re-run MULTI/EXEC in a bounded loop.
  2. Reduce contention by switching to a server-side Lua script for atomicity instead of WATCH/MULTI.
  3. Cap retries and back off to avoid live-lock under sustained contention.
  4. Confirm no other part of your own application is writing the watched key unexpectedly.

Example fix

// before
await client.watch('balance');
const bal = await client.get('balance');
await client.multi().set('balance', Number(bal) - 10).exec(); // throws WatchError on contention

// after
for (let i = 0; i < 10; i++) {
  await client.watch('balance');
  const bal = await client.get('balance');
  try {
    await client.multi().set('balance', Number(bal) - 10).exec();
    break;
  } catch (e) {
    if (!(e instanceof WatchError)) throw e;
  }
}
Defensive patterns

Strategy: retry

Validate before calling

null

Type guard

function isWatchKeyChanged(e: unknown): boolean {
  return e instanceof WatchError && e.message === 'One (or more) of the watched keys has been changed';
}

Try / catch

try {
  await client.multi().set(key, val).exec();
} catch (e) {
  if (e instanceof WatchError) {
    // watched key changed; re-read and retry the read-modify-write
  } else throw e;
}

Prevention

When it happens

Trigger: await client.watch('k'); ...another client or connection modifies 'k'...; await client.multi().set('k', newVal).exec() — EXEC yields null so the library throws WatchError().

Common situations: Concurrent writers to the same key; implementing atomic compare-and-set via WATCH/MULTI; high-contention counters or inventory updates; background jobs mutating keys a foreground transaction watches.

Related errors


AI-assisted analysis of redis/node-redis@90fd0652bc (2026-08-11). Data as JSON: /api/errors/5411ba00c490aebd. Report an issue: GitHub.

Appendix: source

Thrown at packages/client/lib/client/index.ts:1968

            this._self.#queue.addCommand(args, {
              chainId,
              typeMapping,
              slotNumber
            })
          );
        }

        promises.push(
          this._self.#queue.addCommand(['EXEC'], { chainId, slotNumber })
        );

        this._self.#scheduleWrite();

        const results = await Promise.all(promises),
          execResult = results[results.length - 1];

        if (execResult === null) {
          throw new WatchError();
        }

        if (selectedDB !== undefined) {
          this._self.#selectedDB = selectedDB;
        }

        return execResult as Array<unknown>;
      },
      () => ({
        batchMode: 'MULTI' as const,
        batchSize,
        database: this._self.#selectedDB,
        clientId: this._self._clientId,
        ...this._self.#socketTraceContext()
      })
    );
  }

View on GitHub (pinned to 90fd0652bc)