redis/go-redis · critical

redis: NewFailoverClusterClient nil options

Error message

redis: NewFailoverClusterClient nil options

What it means

NewFailoverClusterClient panics when the *FailoverOptions argument is nil. The constructor copies SentinelAddrs and builds a sentinelFailover from the options immediately, so a nil pointer is rejected up front with an explicit panic.

Source

Thrown at sentinel.go:1263

			}
			addr := net.JoinHostPort(parts[3], parts[4])
			c.trySwitchMaster(pubsub.getContext(), addr)
		}

		if c.onUpdate != nil {
			c.onUpdate(ctx)
		}
	}
}

//------------------------------------------------------------------------------

// NewFailoverClusterClient returns a client that supports routing read-only commands
// to a replica node.
// Passing nil FailoverOptions will cause a panic.
func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient {
	if failoverOpt == nil {
		panic("redis: NewFailoverClusterClient nil options")
	}

	sentinelAddrs := make([]string, len(failoverOpt.SentinelAddrs))
	copy(sentinelAddrs, failoverOpt.SentinelAddrs)

	failover := &sentinelFailover{
		opt:           failoverOpt,
		sentinelAddrs: sentinelAddrs,
	}

	opt := failoverOpt.clusterOptions()
	if failoverOpt.DB != 0 {
		onConnect := opt.OnConnect

		opt.OnConnect = func(ctx context.Context, cn *Conn) error {
			if err := cn.Select(ctx, failoverOpt.DB).Err(); err != nil {
				return err
			}

View on GitHub (pinned to c5cad058c7)

Solutions

  1. Provide a populated &redis.FailoverOptions{MasterName: ..., SentinelAddrs: [...]}.
  2. Nil-check the options before calling NewFailoverClusterClient.
  3. If using NewUniversalClient, ensure opts.Failover() (populated UniversalOptions fields) yields non-nil FailoverOptions; leave MasterName empty if not using sentinel.

Example fix

// before
var opt *redis.FailoverOptions
client := redis.NewFailoverClusterClient(opt) // panics

// after
client := redis.NewFailoverClusterClient(&redis.FailoverOptions{
    MasterName:    "mymaster",
    SentinelAddrs: []string{":26379"},
})
Defensive patterns

Strategy: validation

Validate before calling

func validateFailoverClusterOptions(opt *redis.FailoverOptions) error {
    if opt == nil {
        return errors.New("failover cluster options must not be nil")
    }
    if opt.MasterName == "" || len(opt.SentinelAddrs) == 0 {
        return errors.New("MasterName and SentinelAddrs are required")
    }
    return nil
}

Type guard

func hasFailoverClusterOptions(opt *redis.FailoverOptions) bool { return opt != nil }

Try / catch

// Last-resort recover wrapper:
func safeNewFailoverClusterClient(opt *redis.FailoverOptions) (c *redis.ClusterClient, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("NewFailoverClusterClient: %v", r)
        }
    }()
    return redis.NewFailoverClusterClient(opt), nil
}

Prevention

When it happens

Trigger: Calling redis.NewFailoverClusterClient(nil), or with an uninitialized *FailoverOptions. Also reachable via NewUniversalClient when opts.MasterName != "" and RouteByLatency/RouteRandomly/IsClusterMode are set, if UniversalOptions were hand-built with nil Failover fields and MasterName set.

Common situations: Config-driven client factories that select failover-cluster mode but skip options population; a nil FailoverOptions field inside UniversalOptions combined with a non-empty MasterName; test helpers that only partially populate options.

Related errors


AI-assisted analysis of redis/go-redis@c5cad058c7 (2026-09-01). Data as JSON: /api/errors/fb3529c156a076d2. Report an issue: GitHub.