{"record":{"id":"5411ba00c490aebd","repo":"redis/node-redis","slug":"one-or-more-of-the-watched-keys-has-been-changed","errorCode":null,"errorMessage":"One (or more) of the watched keys has been changed","messagePattern":"One \\(or more\\) of the watched keys has been changed","errorType":"exception","errorClass":"WatchError","httpStatus":null,"severity":"warning","filePath":"packages/client/lib/client/index.ts","lineNumber":1968,"sourceCode":"            this._self.#queue.addCommand(args, {\n              chainId,\n              typeMapping,\n              slotNumber\n            })\n          );\n        }\n\n        promises.push(\n          this._self.#queue.addCommand(['EXEC'], { chainId, slotNumber })\n        );\n\n        this._self.#scheduleWrite();\n\n        const results = await Promise.all(promises),\n          execResult = results[results.length - 1];\n\n        if (execResult === null) {\n          throw new WatchError();\n        }\n\n        if (selectedDB !== undefined) {\n          this._self.#selectedDB = selectedDB;\n        }\n\n        return execResult as Array<unknown>;\n      },\n      () => ({\n        batchMode: 'MULTI' as const,\n        batchSize,\n        database: this._self.#selectedDB,\n        clientId: this._self._clientId,\n        ...this._self.#socketTraceContext()\n      })\n    );\n  }\n","sourceCodeStart":1950,"sourceCodeEnd":1986,"githubUrl":"https://github.com/redis/node-redis/blob/90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58/packages/client/lib/client/index.ts#L1950-L1986","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","solutions":["Treat WatchError as a retry signal: re-read the current value, re-issue WATCH, and re-run MULTI/EXEC in a bounded loop.","Reduce contention by switching to a server-side Lua script for atomicity instead of WATCH/MULTI.","Cap retries and back off to avoid live-lock under sustained contention.","Confirm no other part of your own application is writing the watched key unexpectedly."],"exampleFix":"// before\nawait client.watch('balance');\nconst bal = await client.get('balance');\nawait client.multi().set('balance', Number(bal) - 10).exec(); // throws WatchError on contention\n\n// after\nfor (let i = 0; i < 10; i++) {\n  await client.watch('balance');\n  const bal = await client.get('balance');\n  try {\n    await client.multi().set('balance', Number(bal) - 10).exec();\n    break;\n  } catch (e) {\n    if (!(e instanceof WatchError)) throw e;\n  }\n}","handlingStrategy":"retry","validationCode":"null","typeGuard":"function isWatchKeyChanged(e: unknown): boolean {\n  return e instanceof WatchError && e.message === 'One (or more) of the watched keys has been changed';\n}","tryCatchPattern":"try {\n  await client.multi().set(key, val).exec();\n} catch (e) {\n  if (e instanceof WatchError) {\n    // watched key changed; re-read and retry the read-modify-write\n  } else throw e;\n}","preventionTips":["Treat WatchError as an expected control-flow signal, not a fatal error.","Cap retries and back off to avoid live-lock under contention.","Use Lua EVAL for high-contention atomic operations instead of WATCH/MULTI."],"tags":["transactions","watch","multi","contention"],"backgroundTag":null,"analyzedSha":"90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58","analyzedAt":"2026-08-11T15:37:21.243Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}