redis/go-redis · critical

redis: NewFailoverClient nil options

Error message

redis: NewFailoverClient nil options

What it means

NewFailoverClient panics immediately when the *FailoverOptions argument is nil. The constructor requires a non-nil options struct because it reads SentinelAddrs, MasterName and other fields directly to build the client. The library deliberately fails fast instead of returning a half-configured client.

Source

Thrown at sentinel.go:545

	if o.TLSConfig != nil && q.has("skip_verify") {
		o.TLSConfig.InsecureSkipVerify = q.bool("skip_verify")
	}

	// any parameters left?
	if r := q.remaining(); len(r) > 0 {
		return nil, fmt.Errorf("redis: unexpected option: %s", strings.Join(r, ", "))
	}

	return o, nil
}

// NewFailoverClient returns a Redis client that uses Redis Sentinel
// for automatic failover. It's safe for concurrent use by multiple
// goroutines.
// Passing nil FailoverOptions will cause a panic.
func NewFailoverClient(failoverOpt *FailoverOptions) *Client {
	if failoverOpt == nil {
		panic("redis: NewFailoverClient nil options")
	}

	if failoverOpt.RouteByLatency {
		panic("to route commands by latency, use NewFailoverClusterClient")
	}
	if failoverOpt.RouteRandomly {
		panic("to route commands randomly, use NewFailoverClusterClient")
	}

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

	rand.Shuffle(len(sentinelAddrs), func(i, j int) {
		sentinelAddrs[i], sentinelAddrs[j] = sentinelAddrs[j], sentinelAddrs[i]
	})

	failover := &sentinelFailover{
		opt:           failoverOpt,

View on GitHub (pinned to c5cad058c7)

Solutions

  1. Construct a valid &redis.FailoverOptions{...} with at least MasterName and SentinelAddrs set before calling NewFailoverClient.
  2. Add a nil check (or a config loader that never returns nil) before invoking NewFailoverClient.
  3. If sentinel failover is not actually needed, use redis.NewClient(&redis.Options{...}) instead.

Example fix

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

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

Strategy: validation

Validate before calling

func validateFailoverOptions(opt *redis.FailoverOptions) error {
    if opt == nil {
        return errors.New("failover options must not be nil")
    }
    if opt.MasterName == "" || len(opt.SentinelAddrs) == 0 {
        return errors.New("MasterName and SentinelAddrs are required")
    }
    if opt.RouteByLatency || opt.RouteRandomly {
        return errors.New("use NewFailoverClusterClient for routing options")
    }
    return nil
}

Type guard

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

Try / catch

// Go panics are not catchable via try/catch; recover only as a last resort:
func safeNewFailoverClient(opt *redis.FailoverOptions) (c *redis.Client, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("NewFailoverClient: %v", r)
        }
    }()
    return redis.NewFailoverClient(opt), nil
}

Prevention

When it happens

Trigger: Calling redis.NewFailoverClient(nil), or calling it with a *FailoverOptions variable that was declared but never assigned (nil pointer).

Common situations: Building options conditionally in a helper that returns *FailoverOptions and returning nil on a config-parse failure; loading options from env/flags where the failover section is missing; passing a nil pointer through a factory or NewUniversalClient path that forwards opts.Failover() results.

Related errors


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