redis/node-redis · error · WatchError

Client reconnected after WATCH

Error message

Client reconnected after WATCH

What it means

Thrown from _executeMulti (client/index.ts:1932) when the socket epoch recorded at WATCH time no longer equals the current socketEpoch. The epoch increments on every reconnect, so a mismatch means the underlying TCP connection dropped and was re-established between WATCH and EXEC. Redis discards all watches when the connection closes, so the client cannot honor the optimistic lock and fails fast instead of silently running EXEC unguarded.

Source

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

    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(
            this._self.#queue.addCommand(args, {
              chainId,
              typeMapping
            })

View on GitHub (pinned to bb5beb5657)

Solutions

  1. Catch WatchError and replay the WATCH + transaction from the start on the fresh connection.
  2. Stabilize the connection: raise socketTimeout, tune reconnectStrategy, or fix the underlying network.
  3. Shorten the WATCH→EXEC window so reconnects are less likely to land inside it.

Example fix

// before
await client.watch('k');
// ... network blip triggers reconnect ...
await client.multi().set('k', 'v').exec(); // throws WatchError('Client reconnected after WATCH')

// after
async function optimisticTxn(client) {
  for (;;) {
    await client.watch('k');
    try {
      return await client.multi().set('k', 'v').exec();
    } catch (e) {
      if (/reconnected after WATCH/.test(e.message)) continue;
      throw e;
    }
  }
}
Defensive patterns

Strategy: retry

Validate before calling

if (client.isWatching && client.socketEpoch !== /* captured at watch */) {
  // connection changed since WATCH; expect WatchError
}

Type guard

function isReconnectWatchError(e: unknown): boolean {
  return e instanceof Error && /reconnected after WATCH/i.test(e.message);
}

Try / catch

for (let i = 0; i < N; i++) {
  await client.watch('k');
  try { return await client.multi().set('k','v').exec(); }
  catch (e) { if (isReconnectWatchError(e) && i < N-1) continue; throw e; }
}

Prevention

When it happens

Trigger: Call client.WATCH(key); a network blip/reconnect happens before multi().exec() resolves; the epoch check at EXEC time throws WatchError('Client reconnected after WATCH').

Common situations: Unstable network or a Redis server restart between WATCH and EXEC; long transactions on flaky links; aggressive socket timeouts that trigger reconnects mid-transaction.

Related errors


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