{"record":{"id":"602f64df4fb77554","repo":"redis/redis-py","slug":"a-type-error-name-occurred-while-watching-o","errorCode":null,"errorMessage":"A {type(error).__name__} occurred while watching one or more keys","messagePattern":"A (.+?) occurred while watching one or more keys","errorType":"exception","errorClass":"WatchError","httpStatus":null,"severity":"error","filePath":"redis/asyncio/client.py","lineNumber":1947,"sourceCode":"            and failure_count is not None\n            and failure_count <= conn.retry.get_retries()\n        ):\n            await record_operation_duration(\n                command_name=command_name,\n                duration_seconds=time.monotonic() - start_time,\n                server_address=getattr(conn, \"host\", None),\n                server_port=getattr(conn, \"port\", None),\n                db_namespace=str(conn.db),\n                error=error,\n                retry_attempts=failure_count,\n            )\n        await conn.disconnect(error=error, failure_count=failure_count)\n        # if we were already watching a variable, the watch is no longer\n        # valid since this connection has died. raise a WatchError, which\n        # indicates the user should retry this transaction.\n        if self.watching:\n            await self.reset()\n            raise WatchError(\n                f\"A {type(error).__name__} occurred while watching one or more keys\"\n            )\n\n    async def immediate_execute_command(self, *args, **options):\n        \"\"\"\n        Execute a command immediately, but don't auto-retry on the supported\n        errors for retry if we're already WATCHing a variable.\n        Used when issuing WATCH or subsequent commands retrieving their values but before\n        MULTI is called.\n        \"\"\"\n        command_name = args[0]\n        conn = self.connection\n        # if this is the first call, we need a connection\n        if not conn:\n            conn = await self.connection_pool.get_connection()\n            self.connection = conn\n\n        # Start timing for observability","sourceCodeStart":1929,"sourceCodeEnd":1965,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/asyncio/client.py#L1929-L1965","documentation":"Raised by Pipeline._disconnect_reset_raise_on_watching during the immediate-command path (WATCH + pre-MULTI reads) when the connection fails with a retryable error and self.watching is True. Because a watched key's validity is tied to the connection, any disconnect invalidates the WATCH; the library resets state and raises WatchError so the caller retries the whole transaction.","triggerScenarios":"Issuing WATCH then a read (e.g. await pipe.get(key)) inside a multi-step transaction, and the socket raises ConnectionError / TimeoutError that the retry layer could not recover within conn.retry.get_retries(). The f-string names the underlying error class (e.g. ConnectionError).","commonSituations":"Network blips or a Redis restart mid-transaction; aggressive socket_timeout on long WATCH+read sequences; containerized deployments where the load balancer closes idle connections between WATCH and EXEC.","solutions":["Wrap the WATCH->read->MULTI->EXEC sequence in a retry loop that catches WatchError and re-runs the entire transaction.","Raise conn.retry retries (Retry(backoff=..., retries=N)) to absorb transient errors before they surface as WatchError.","Increase socket_timeout if the read between WATCH and MULTI is slow."],"exampleFix":"// before\nasync with client.pipeline(transaction=True) as pipe:\n    await pipe.watch('k')\n    v = await pipe.get('k')\n    pipe.multi()\n    await pipe.set('k', process(v))\n    await pipe.execute()\n// after\nfor _ in range(max_attempts):\n    try:\n        async with client.pipeline(transaction=True) as pipe:\n            await pipe.watch('k')\n            v = await pipe.get('k')\n            pipe.multi()\n            await pipe.set('k', process(v))\n            await pipe.execute()\n        break\n    except WatchError:\n        continue","handlingStrategy":"retry","validationCode":"# No pure-client precondition for a network failure mid-WATCH.\n# The defense is a retry loop around the whole transaction.","typeGuard":null,"tryCatchPattern":"from redis.exceptions import WatchError\nfor _ in range(max_attempts):\n    try:\n        async with client.pipeline(transaction=True) as pipe:\n            await pipe.watch('k')\n            v = await pipe.get('k')\n            pipe.multi()\n            await pipe.set('k', process(v))\n            await pipe.execute()\n        break\n    except WatchError:\n        continue","preventionTips":["Always wrap WATCH-based transactions in a bounded retry loop.","Tune Retry(retries=...) and socket_timeout to absorb transient network errors."],"tags":["pipeline","watch","connection","transaction","retry"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}