{"id":"b0f116fbecac765b","repo":"go-redis/redis","slug":"redis-watch-requires-at-least-one-shard","errorCode":null,"errorMessage":"redis: Watch requires at least one shard","messagePattern":"redis: Watch requires at least one shard","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ring.go","lineNumber":982,"sourceCode":"\tif len(keys) == 0 {\n\t\treturn fmt.Errorf(\"redis: Watch requires at least one key\")\n\t}\n\n\tvar shards []*ringShard\n\n\tfor _, key := range keys {\n\t\tif key != \"\" {\n\t\t\tshard, err := c.sharding.GetByKey(key)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tshards = append(shards, shard)\n\t\t}\n\t}\n\n\tif len(shards) == 0 {\n\t\treturn fmt.Errorf(\"redis: Watch requires at least one shard\")\n\t}\n\n\tif len(shards) > 1 {\n\t\tfor _, shard := range shards[1:] {\n\t\t\tif shard.Client != shards[0].Client {\n\t\t\t\terr := fmt.Errorf(\"redis: Watch requires all keys to be in the same shard\")\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn shards[0].Client.Watch(ctx, fn, keys...)\n}\n\n// Close closes the ring client, releasing any open resources.\n//\n// It is rare to Close a Ring, as the Ring is meant to be long-lived\n// and shared between many goroutines.","sourceCodeStart":964,"sourceCodeEnd":1000,"githubUrl":"https://github.com/go-redis/redis/blob/36d97525cd8076aed67cddf54778e9ea84550929/ring.go#L964-L1000","documentation":"Returned by Ring.Watch when keys were supplied but none of them resolved to a shard. Watch filters out empty-string keys before resolving shards; if every key is \"\" the shards slice stays empty and the transaction cannot proceed because there is no shard to lock.","triggerScenarios":"Calling ring.Watch(ctx, fn, keys...) where every element of keys is an empty string. The loop appends a shard only for non-empty keys, so an all-empty slice yields zero shards.","commonSituations":"Keys slice populated from a struct/JSON that had missing fields; a map iteration producing zero-length values; a trim/normalization step that reduced all keys to \"\"; accidental placeholder keys.","solutions":["Sanitize the keys slice: drop empty strings and verify at least one non-empty key remains before calling Watch.","Fix the upstream producer of the keys so it never yields empty values.","Add a unit test asserting keys are non-empty at the Watch call site."],"exampleFix":"// before\nkeys := []string{\"\", \"\", \"\"}\nerr := ring.Watch(ctx, fn, keys...)\n\n// after\nvar nonEmpty []string\nfor _, k := range keys {\n    if k != \"\" {\n        nonEmpty = append(nonEmpty, k)\n    }\n}\nif len(nonEmpty) == 0 {\n    return errors.New(\"no valid keys\")\n}\nerr := ring.Watch(ctx, fn, nonEmpty...)","handlingStrategy":"validation","validationCode":"var nonEmpty []string\nfor _, k := range keys {\n    if k != \"\" {\n        nonEmpty = append(nonEmpty, k)\n    }\n}\nif len(nonEmpty) == 0 {\n    return errors.New(\"no non-empty keys to watch\")\n}\nreturn ring.Watch(ctx, fn, nonEmpty...)","typeGuard":null,"tryCatchPattern":"if err := ring.Watch(ctx, fn, keys...); err != nil {\n    if strings.Contains(err.Error(), \"requires at least one shard\") {\n        // all keys were empty; sanitize upstream\n    }\n}","preventionTips":["Sanitize key lists at the boundary where they enter your code.","Reject empty keys at the data-ingestion layer rather than at the Redis call site.","Add property tests asserting keys are non-empty before Watch."],"tags":["ring","transaction","validation"],"analyzedSha":"36d97525cd8076aed67cddf54778e9ea84550929","analyzedAt":"2026-08-06T01:08:27.376Z","schemaVersion":2}