redis/redis-py · error · DataError

XNACK retrycount must be >= 0

Error message

XNACK retrycount must be >= 0

What it means

Raised by xnack() when optional `retrycount` is negative. CAUTION: unlike xclaim, there is NO isinstance check here - only `retrycount < 0` (core.py:7689). So a negative int raises DataError correctly, but a non-numeric retrycount (e.g. a string) makes `str < 0` raise an uncaught TypeError that crashes the call.

Source

Thrown at redis/commands/core.py:7690

                already in the group's PEL.

        Returns:
            The number of messages successfully NACKed.

        For more information, see https://redis.io/commands/xnack
        """
        if not ids:
            raise DataError("XNACK requires at least one message ID")

        if mode not in {"SILENT", "FAIL", "FATAL"}:
            raise DataError("XNACK mode must be one of: SILENT, FAIL, FATAL")

        pieces: list = [name, groupname, mode, "IDS", len(ids)]
        pieces.extend(ids)

        if retrycount is not None:
            if retrycount < 0:
                raise DataError("XNACK retrycount must be >= 0")
            pieces.extend([b"RETRYCOUNT", retrycount])

        if force:
            pieces.append(b"FORCE")

        return self.execute_command("XNACK", *pieces)

    @overload
    def xpending(
        self: SyncClientProtocol, name: KeyT, groupname: GroupT
    ) -> dict[str, Any]: ...

    @overload
    def xpending(
        self: AsyncClientProtocol, name: KeyT, groupname: GroupT
    ) -> Awaitable[dict[str, Any]]: ...

    def xpending(

View on GitHub (pinned to da03cdc7e8)

Solutions

  1. Pass a non-negative int, or None to let the mode decide.
  2. Coerce and clamp: rc = None if raw is None else max(0, int(raw)).
  3. Do NOT pass a string - it will crash with TypeError before this DataError can fire.

Example fix

# before
r.xnack('s','g','FAIL', *ids, retrycount=attempts)  # attempts == -1
# after
r.xnack('s','g','FAIL', *ids, retrycount=None if attempts is None else max(0, int(attempts)))
Defensive patterns

Strategy: validation

Validate before calling

rc = None if retrycount is None else max(0, int(retrycount))
r.xnack(name, group, mode, *ids, retrycount=rc)

Type guard

def opt_nonneg_int(v):
    return None if v is None else max(0, int(v))

Try / catch

from redis.exceptions import DataError
try:
    r.xnack(name, group, mode, *ids, retrycount=rc)
except DataError:
    r.xnack(name, group, mode, *ids, retrycount=max(0, int(rc)))

Prevention

When it happens

Trigger: xnack(..., retrycount=-1) (int) -> DataError. xnack(..., retrycount='-1') (str) -> TypeError, NOT this DataError. retrycount=None or >=0 is fine.

Common situations: Passing retrycount from JSON as a string; a decremented counter that went below zero; expecting the guard to also validate type.

Related errors


AI-assisted analysis of redis/redis-py@da03cdc7e8 (2026-08-04). Data as JSON: /data/errors/1a4363c8bafb59d4.json. Report an issue: GitHub.