{"record":{"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":1936,"sourceCode":"    chainId = Symbol('MULTI Chain')\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 promises: Array<Promise<unknown>> = [\n          this._self.#queue.addCommand(['MULTI'], { chainId, slotNumber }),\n        ];\n\n        for (const { args } of commands) {\n          promises.push(\n            this._self.#queue.addCommand(args, {\n              chainId,\n              typeMapping,\n              slotNumber\n            })","sourceCodeStart":1918,"sourceCodeEnd":1954,"githubUrl":"https://github.com/redis/node-redis/blob/90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58/packages/client/lib/client/index.ts#L1918-L1954","documentation":"Thrown in _executeMulti when the socketEpoch recorded at WATCH time no longer matches the current socketEpoch. socketEpoch increments on every successful (re)connection, so a reconnect between WATCH and EXEC invalidates the optimistic lock because the new connection has no WATCH registered on the server. The library refuses to send a transaction whose safety it cannot guarantee.","triggerScenarios":"Calling await client.watch('key'), then the connection drops and auto-reconnects (socketEpoch++), then calling await multi.exec(). Any network interruption, Redis restart, or failover that triggers a reconnect inside the WATCH..EXEC window.","commonSituations":"Long-running transactions held open while the network is unstable; Redis Sentinel/Cluster failover between WATCH and EXEC; socketTimeout triggering a reconnect mid-transaction; environments with intermittent TCP resets.","solutions":["Wrap the WATCH + MULTI/EXEC sequence in a retry loop that re-issues WATCH on the new connection after a reconnect.","Keep the WATCH-to-EXEC window as short as possible; queue all commands and call exec() immediately.","Catch WatchError and distinguish message === 'Client reconnected after WATCH' from a real key-change violation, retrying the former unconditionally.","Move atomic compare-and-set logic into a server-side Lua script to avoid the WATCH reconnect fragility entirely."],"exampleFix":"// before\nawait client.watch('key');\n// ... network blip causes reconnect ...\nawait client.multi().set('k','v').exec(); // WatchError('Client reconnected after WATCH')\n\n// after\nfor (let attempt = 0; attempt < 5; attempt++) {\n  await client.watch('key');\n  try {\n    return await client.multi().set('k','v').exec();\n  } catch (e) {\n    if (e instanceof WatchError) continue; // reconnect or key-change: retry\n    throw e;\n  }\n}","handlingStrategy":"retry","validationCode":"null","typeGuard":"function isReconnectWatchError(e: unknown): boolean {\n  return e instanceof WatchError && e.message === 'Client reconnected after WATCH';\n}","tryCatchPattern":"for (let i = 0; i < MAX; i++) {\n  await client.watch(key);\n  try {\n    return await client.multi().set(key, val).exec();\n  } catch (e) {\n    if (e instanceof WatchError) continue; // reconnect or key change\n    throw e;\n  }\n}","preventionTips":["Keep the WATCH-to-EXEC window minimal; queue commands and exec() immediately.","Always wrap WATCH/MULTI/EXEC in a retry loop; reconnects are always possible.","Prefer Lua scripts for atomic CAS to avoid WATCH reconnect fragility."],"tags":["transactions","watch","reconnect","multi"],"backgroundTag":null,"analyzedSha":"90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58","analyzedAt":"2026-08-11T15:37:21.243Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}