{"id":"52c9622b17d95c49","repo":"go-redis/redis","slug":"at-least-one-channel-is-required","errorCode":null,"errorMessage":"at least one channel is required","messagePattern":"at least one channel is required","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ring.go","lineNumber":711,"sourceCode":"\t\ts := shard.Client.connPool.Stats()\n\t\tacc.Hits += s.Hits\n\t\tacc.Misses += s.Misses\n\t\tacc.Timeouts += s.Timeouts\n\t\tacc.TotalConns += s.TotalConns\n\t\tacc.IdleConns += s.IdleConns\n\t}\n\treturn &acc\n}\n\n// Len returns the current number of shards in the ring.\nfunc (c *Ring) Len() int {\n\treturn c.sharding.Len()\n}\n\n// Subscribe subscribes the client to the specified channels.\nfunc (c *Ring) Subscribe(ctx context.Context, channels ...string) *PubSub {\n\tif len(channels) == 0 {\n\t\tpanic(\"at least one channel is required\")\n\t}\n\n\tshard, err := c.sharding.GetByKey(channels[0])\n\tif err != nil {\n\t\t// TODO: return PubSub with sticky error\n\t\tpanic(err)\n\t}\n\treturn shard.Client.Subscribe(ctx, channels...)\n}\n\n// PSubscribe subscribes the client to the given patterns.\nfunc (c *Ring) PSubscribe(ctx context.Context, channels ...string) *PubSub {\n\tif len(channels) == 0 {\n\t\tpanic(\"at least one channel is required\")\n\t}\n\n\tshard, err := c.sharding.GetByKey(channels[0])\n\tif err != nil {","sourceCodeStart":693,"sourceCodeEnd":729,"githubUrl":"https://github.com/go-redis/redis/blob/36d97525cd8076aed67cddf54778e9ea84550929/ring.go#L693-L729","documentation":"Ring.Subscribe panics with 'at least one channel is required' when called with zero channel arguments (ring.go:710-712). The Ring is client-side sharded, so the first channel name is used (via c.sharding.GetByKey(channels[0])) to pick the shard that owns the subscription; with no channels there is no key to route by, hence the guard. This is a programmer error and is enforced with a panic rather than an error return.","triggerScenarios":"Calling ring.Subscribe(ctx) with no variadic channel arguments; passing an empty []string slice; passing a slice that was built dynamically and ended up empty due to a filter/config miss.","commonSituations":"Dynamic channel lists sourced from config/env that came back empty; helper functions that forward a channels slice without checking length; tests that forgot to populate the channel list; conditional logic that subscribes only when a feature flag is set, but the Subscribe call still runs.","solutions":["Validate len(channels) > 0 before calling ring.Subscribe and skip/return an error when empty.","Ensure your config/source for channel names always yields at least one entry, or guard the subscription step behind a feature/config check.","Wrap Subscribe in a helper that returns an error on empty input instead of letting the Ring panic."],"exampleFix":"// before\nring.Subscribe(ctx, channels...) // channels is empty -> panic\n\n// after\nif len(channels) == 0 {\n    return errors.New(\"no channels to subscribe\")\n}\nsub := ring.Subscribe(ctx, channels...)","handlingStrategy":"validation","validationCode":"func subscribeRing(ctx context.Context, r *redis.Ring, channels []string) (*redis.PubSub, error) {\n    if len(channels) == 0 {\n        return nil, errors.New(\"at least one channel is required\")\n    }\n    return r.Subscribe(ctx, channels...), nil\n}","typeGuard":"func nonEmptyChannels(channels []string) bool {\n    return len(channels) > 0\n}","tryCatchPattern":"defer func() {\n    if r := recover(); r != nil {\n        log.Printf(\"ring subscribe failed: %v\", r)\n    }\n}()\nsub := r.Subscribe(ctx, channels...)","preventionTips":["Check len(channels) > 0 before calling Subscribe.","Make config-driven channel lists default to at least one entry or skip the subscription step.","Forward variadic args through a validating helper.","Add tests that exercise the empty-input case explicitly."],"tags":["ring","pubsub","panic","validation","subscribe"],"analyzedSha":"36d97525cd8076aed67cddf54778e9ea84550929","analyzedAt":"2026-08-06T01:08:27.376Z","schemaVersion":2}