redis/go-redis · critical

redis: NewUniversalClient nil options

Error message

redis: NewUniversalClient nil options

What it means

NewUniversalClient panics when the *UniversalOptions argument is nil. UniversalOptions drives which concrete client (failover-cluster, failover, cluster, or single-node) is built, and the constructor reads its fields immediately, so nil is rejected with a panic as documented on the function.

Source

Thrown at universal.go:448

	// delegate to the underlying client.
	_ UniversalClient = (*AutoPipeliner)(nil)
)

// NewUniversalClient returns a new multi client. The type of the returned client depends
// on the following conditions:
//
//  1. If the MasterName option is specified with RouteByLatency, RouteRandomly or IsClusterMode,
//     a FailoverClusterClient is returned.
//  2. If the MasterName option is specified without RouteByLatency, RouteRandomly or IsClusterMode,
//     a sentinel-backed FailoverClient is returned.
//  3. If the number of Addrs is two or more, or IsClusterMode option is specified,
//     a ClusterClient is returned.
//  4. Otherwise, a single-node Client is returned.
//
// Passing nil UniversalOptions will cause a panic.
func NewUniversalClient(opts *UniversalOptions) UniversalClient {
	if opts == nil {
		panic("redis: NewUniversalClient nil options")
	}

	switch {
	case opts.MasterName != "" && (opts.RouteByLatency || opts.RouteRandomly || opts.IsClusterMode):
		return NewFailoverClusterClient(opts.Failover())
	case opts.MasterName != "":
		return NewFailoverClient(opts.Failover())
	case len(opts.Addrs) > 1 || opts.IsClusterMode:
		return NewClusterClient(opts.Cluster())
	default:
		return NewClient(opts.Simple())
	}
}

View on GitHub (pinned to c5cad058c7)

Solutions

  1. Construct a &redis.UniversalOptions{Addrs: []string{"host:6379"}} (or with MasterName/SentinelAddrs for failover) before calling NewUniversalClient.
  2. Add a nil guard in the factory that builds the client, returning a configuration error instead of invoking the constructor.
  3. Ensure your config unmarshaling allocates the UniversalOptions struct (value type, not nil pointer) even when fields are empty.

Example fix

// before
var opts *redis.UniversalOptions
client := redis.NewUniversalClient(opts) // panics

// after
client := redis.NewUniversalClient(&redis.UniversalOptions{
    Addrs: []string{"localhost:6379"},
})
Defensive patterns

Strategy: validation

Validate before calling

func validateUniversalOptions(opts *redis.UniversalOptions) error {
    if opts == nil {
        return errors.New("universal options must not be nil")
    }
    if opts.MasterName == "" && len(opts.Addrs) == 0 {
        return errors.New("either Addrs or MasterName+SentinelAddrs are required")
    }
    return nil
}

Type guard

func hasUniversalOptions(opts *redis.UniversalOptions) bool { return opts != nil }

Try / catch

// Last-resort recover wrapper:
func safeNewUniversalClient(opts *redis.UniversalOptions) (c redis.UniversalClient, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("NewUniversalClient: %v", r)
        }
    }()
    return redis.NewUniversalClient(opts), nil
}

Prevention

When it happens

Trigger: Calling redis.NewUniversalClient(nil); passing a *UniversalOptions obtained from a config loader that returned nil when no redis section was present.

Common situations: Generic client factories in application frameworks that forward a nil options struct when configuration is absent or failed to parse; struct-typed config parsed from YAML/env left as nil pointer when unset.

Related errors


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