{"id":"8f9a3b4fa7c37782","repo":"redis/node-redis","slug":"client-reconnected-after-watch","errorCode":null,"errorMessage":"Client reconnected after WATCH","messagePattern":"Client reconnected after WATCH","errorType":"exception","errorClass":"WatchError","httpStatus":null,"severity":"error","filePath":"packages/client/lib/client/index.ts","lineNumber":1932,"sourceCode":"    selectedDB?: number\n  ) {\n    assertNoHimportSessionCommands(commands);\n\n    const dirtyWatch = this._self.#dirtyWatch;\n    this._self.#dirtyWatch = undefined;\n    const watchEpoch = this._self.#watchEpoch;\n    this._self.#watchEpoch = undefined;\n\n    if (!this._self.#socket.isOpen) {\n      throw new ClientClosedError();\n    }\n\n    if (dirtyWatch) {\n      throw new WatchError(dirtyWatch);\n    }\n\n    if (watchEpoch && watchEpoch !== this._self.socketEpoch) {\n      throw new WatchError('Client reconnected after WATCH');\n    }\n\n    const batchSize = commands.length;\n\n    return trace(CHANNELS.TRACE_BATCH,\n      async () => {\n        const typeMapping = this._commandOptions?.typeMapping;\n        const chainId = Symbol('MULTI Chain');\n        const promises: Array<Promise<unknown>> = [\n          this._self.#queue.addCommand(['MULTI'], { chainId }),\n        ];\n\n        for (const { args } of commands) {\n          promises.push(\n            this._self.#queue.addCommand(args, {\n              chainId,\n              typeMapping\n            })","sourceCodeStart":1914,"sourceCodeEnd":1950,"githubUrl":"https://github.com/redis/node-redis/blob/bb5beb56578573910e2ee8f39681edc214c41398/packages/client/lib/client/index.ts#L1914-L1950","documentation":"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.","triggerScenarios":"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').","commonSituations":"Unstable network or a Redis server restart between WATCH and EXEC; long transactions on flaky links; aggressive socket timeouts that trigger reconnects mid-transaction.","solutions":["Catch WatchError and replay the WATCH + transaction from the start on the fresh connection.","Stabilize the connection: raise socketTimeout, tune reconnectStrategy, or fix the underlying network.","Shorten the WATCH→EXEC window so reconnects are less likely to land inside it."],"exampleFix":"// before\nawait client.watch('k');\n// ... network blip triggers reconnect ...\nawait client.multi().set('k', 'v').exec(); // throws WatchError('Client reconnected after WATCH')\n\n// after\nasync function optimisticTxn(client) {\n  for (;;) {\n    await client.watch('k');\n    try {\n      return await client.multi().set('k', 'v').exec();\n    } catch (e) {\n      if (/reconnected after WATCH/.test(e.message)) continue;\n      throw e;\n    }\n  }\n}","handlingStrategy":"retry","validationCode":"if (client.isWatching && client.socketEpoch !== /* captured at watch */) {\n  // connection changed since WATCH; expect WatchError\n}","typeGuard":"function isReconnectWatchError(e: unknown): boolean {\n  return e instanceof Error && /reconnected after WATCH/i.test(e.message);\n}","tryCatchPattern":"for (let i = 0; i < N; i++) {\n  await client.watch('k');\n  try { return await client.multi().set('k','v').exec(); }\n  catch (e) { if (isReconnectWatchError(e) && i < N-1) continue; throw e; }\n}","preventionTips":["Stabilize the link (socketTimeout, reconnectStrategy) to avoid mid-transaction reconnects.","Replay the whole WATCH+EXEC on reconnect-style WatchError.","Avoid holding WATCH open across long awaits."],"tags":["transactions","watch","reconnect","network","optimistic-locking"],"analyzedSha":"bb5beb56578573910e2ee8f39681edc214c41398","analyzedAt":"2026-08-03T19:09:15.686Z","schemaVersion":2}