Excessive nil pointer checks in Go

ingve1 pts1 comments

Excessive nil pointer checks in Go<br>Excessive nil pointer checks in Go

Jun 16, 2026Let&rsquo;s talk about nil pointer checks in Go. You want to prevent panics in production, but that doesn&rsquo;t start with a deferred recover. It starts with defensive programming. Check your inputs, check your bounds, and check pointers for nil before dereferencing them.<br>I&rsquo;ve started to see more nil checks in Go code. In the right place, they are necessary for writing safe code. In the wrong place, they are a sign that the code has stopped being clear about what can and cannot be nil. I have noticed this pattern more in generated code, but this symptom is not new and is not limited to AI.<br>When a nil check is cheap and prevents a panic, why not add it? Your reflex may be, let&rsquo;s just be safe. But the check also tells the next reader something, and often the wrong thing.<br>Nil Check on a Dependency<br>Consider a nil check on a dependency. A type RateLimiter holds a Redis client, and a nil check guards before using it in lines 10 to 12:<br>1type RateLimiter struct {<br>2 redis *redis.Client<br>3}<br>5func (r *RateLimiter) Allow(ctx context.Context, req *Request) (bool, error) {<br>6 userID := GetUserID(req)<br>7 if userID == "" {<br>8 return false, nil<br>9 }<br>10 if r.redis != nil {<br>11 return r.checkLimit(ctx, userID)<br>12 }<br>13 return false, nil<br>14}

At a glance, it looks like the safe thing to do, but you should ask yourself: why would you construct RateLimiter with a nil dependency?<br>If the Redis client is nil, the error happened earlier, at construction time. Checking for it now does not handle that error. Worse, it treats operating with the failed construction as an acceptable state. In Go, you want to fail fast and fail early.<br>A nil check is not always defensive programming. In this example, it&rsquo;s a sign that the code has lost track of the lineages of its objects. We no longer know where the pointer came from, who was responsible for initializing it, or what invariant should have made nil impossible.<br>Nil Check on a Dependency in the Constructor<br>You may attempt to address this by pushing the problem up one layer. You now check for nil and return an error to flag the nil dependency as an invalid state.<br>func NewRateLimiter(client *redis.Client) (*RateLimiter, error) {<br>if client == nil {<br>return nil, errors.New("redis client is nil")<br>return &RateLimiter{<br>redis: client,<br>}, nil

It&rsquo;s better, but it&rsquo;s still not correct. Why not? Because we still allowed the invalid state to enter our system. A nil pointer is still being passed to our function, which puts the burden of deciding whether to trust the input on code that should have received a valid value in the first place.<br>The constructor is not where the error happened. The error happens at the initialization site:<br>redisClient, err := NewRedisClient(addr)<br>if err != nil {<br>return nil, err

limiter := NewRateLimiter(redisClient)

Once initialization fails, we should handle that error immediately. We should not continue with a nil pointer and force the next, deeper layer to rediscover the outcome. Doing so also removes the need for the rate limiter constructor to return an error in the first place!<br>If the system needs to tolerate the store being temporarily unavailable, do not propagate a nil, model it explicitly. Wrap it so the outer type is always non-nil and handles retries or degradation internally, exposing methods that are safe to call. The caller gets a type guaranteed to exist, and the messiness stays contained inside it. That is how you encapsulate complexity rather than expose your entire codebase to it.<br>This is the same idea as a database constraint. A NOT NULL or foreign-key constraint guarantees a bad row cannot exist in the first place, so every query can trust the data without re-checking it. You want the same guarantee for your runtime values, with one difference. The database enforces its constraint on every write. You establish yours once, so the rest of the code can rely on it without having to repeat the checks.<br>The Cost of Silent Failures<br>When I flag code like the one above, I often get a response that reveals a well-intentioned instinct about reliability, and it goes something like this:<br>&ldquo;I don&rsquo;t want to return an error here and risk taking the program down over my small change. Wrapping it in a nil check or just logging it feels safer.&rdquo;

The choice feels like crash (bad) versus continue (safe), but it&rsquo;s actually loud failure (safe) versus silent failure (bad). An error explicitly returned is:<br>Loud : You find out it happened.<br>Immediate : You find out near the cause.<br>Attributable : The caller can connect the failure to the operation that failed.<br>An error you swallow is the exact inverse. The failure becomes:<br>Silent : Nothing tells you it happened.<br>Delayed : It surfaces later, after more code has run.<br>Ambiguous : By the time you see the symptom, the original cause is harder to identify.<br>The gap between cause and symptom is the cost, and it grows...

check error rsquo code return redis

Related Articles