Question
My Go program panics with the following error when it runs on a machine that cannot reach the API servers:
panic: runtime error: invalid memory address or nil pointer dereference
The stack trace points to getBody, which sends an HTTP request with client.Do(req). I expected the function to return an error when the API server is unreachable. Why does it panic instead, and how should the HTTP response and error be handled safely?
res, err := client.Do(req)
defer res.Body.Close()
if err != nil {
return nil, err
}
Short Answer
You will learn what a nil pointer dereference is in Go, why http.Client.Do can return a nil response when it returns an error, and why errors must be checked before accessing or deferring work on returned pointer values. You will also see a safer HTTP helper function and related validation practices.
Concept
A nil pointer dereference happens when code tries to access data through a pointer whose value is nil.
In this case:
res, err := client.Do(req)
defer res.Body.Close()
client.Do(req) returns two values:
func (c *Client) Do(req *Request) (*Response, error)
If the server cannot be reached—for example, due to a refused connection, missing network route, or DNS failure—err is non-nil. In this failure case, res is typically nil.
The next line immediately evaluates res.Body.Close() in order to register the deferred call. Because res is nil, accessing res.Body panics before the later if err != nil check can run.
The important rule is:
Check an error before using another return value that may be invalid when that error is non-nil.
This is especially common with functions returning a pointer and an error, such as HTTP requests, file opening, database queries, and network connections.
Mental Model
Think of res as a package delivered by a courier and err as the delivery status.
- When delivery succeeds, you receive a package (
res) and no problem (err == nil). - When delivery fails, there is no package (
res == nil) and the courier gives you an explanation (err != nil).
Calling res.Body.Close() before inspecting err is like trying to open a package before checking whether it was delivered. If no package exists, the program crashes.
Check the delivery status first. Only then open and later close the package.
Syntax and Examples
Functions often return a value plus an error:
value, err := someOperation()
if err != nil {
return err
}
// It is safe to use value here.
use(value)
For an HTTP request, the safe ordering is:
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
After err is checked, res can safely be used. Deferring res.Body.Close() is still important: it ensures that the response body is closed when the function returns, whether reading succeeds or fails.
A complete small example:
package main
import (
"fmt"
"net/http"
)
func statusCode(url string) (int, error) {
res, err := http.Get(url)
if err != nil {
return 0, fmt.Errorf("request failed: %w", err)
}
defer res.Body.Close()
return res.StatusCode, nil
}
func main() {
code, err := statusCode("http://127.0.0.1:9999")
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Status:", code)
}
If nothing is listening on port 9999, the program prints an error rather than panicking.
Step by Step Execution
Consider this safe version:
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
When the API server is unreachable:
client.Do(req)attempts to create a network connection.- The connection fails.
client.Doreturnsres == niland a non-nilerr.- The
if err != nilcondition is true. - The function returns that error to its caller.
defer res.Body.Close()is never reached, so no nil pointer is accessed.
When the API server responds successfully:
client.Do(req)returns a non-nilresanderr == nil.- The error check is skipped.
defer res.Body.Close()is registered.- The body is read.
- When the function returns,
res.Body.Close()runs automatically.
In contrast, the original order evaluates res.Body.Close() immediately after client.Do, before it knows whether res exists.
Real World Use Cases
This pattern applies anywhere an operation can fail while returning a value.
- HTTP APIs: Check the error before reading or closing
res.Body. - Files: Check the error from
os.Openbefore calling methods on the returned file. - Databases: Check errors from connection or query operations before using returned objects.
- Network sockets: Check errors from
net.Dialbefore reading from or writing to a connection. - JSON data: Validate that arrays contain required elements before using indexes such as
items[0]. - External commands: Handle the error from
cmd.Run()so callers can decide whether to retry, log, or stop.
For services that depend on remote systems, good error handling turns network outages into useful messages instead of application crashes.
Real Codebase Usage
In production Go code, developers usually combine early returns, cleanup, context, and descriptive wrapped errors.
func fetchBody(ctx context.Context, client *http.Client, method, url string, headers map[string]string, body []byte) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
for key, value := range headers {
req.Header.Set(key, value)
}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("send %s request to %s: %w", method, url, err)
}
defer res.Body.Close()
responseBody, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("read response body: %w", err)
}
if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusMultipleChoices {
return nil, fmt.Errorf("unexpected HTTP status %s: %s", res.Status, string(responseBody))
}
return responseBody, nil
}
Common project-level practices include:
- Use early returns to keep the successful path unindented.
- Use
%wwithfmt.Errorfso callers can inspect wrapped errors usingerrors.Isorerrors.As. - Configure an HTTP client timeout; otherwise some requests can wait too long.
- Use
context.Contextto allow cancellation and request deadlines. - Treat every 2xx status code as successful unless the API specifically requires only
200 OK. - Read an error response body when useful, because APIs often include diagnostic JSON or text.
Common Mistakes
Deferring cleanup before checking the error
Broken:
res, err := client.Do(req)
defer res.Body.Close()
if err != nil {
return nil, err
}
Fix:
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
Checking the wrong error
The original logic checks err in an else if after the request has already succeeded:
if res.StatusCode == 200 {
bodyBytes, err = ioutil.ReadAll(res.Body)
} else if err != nil {
return nil, err
}
That else if err != nil does not check an HTTP status error. It checks the old error from client.Do, which is already nil if execution reached res.StatusCode.
Read the body first, check the read error, then evaluate the status:
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
}
Creating a slice with length when only capacity is intended
This creates ten actual empty-string elements:
imageList := make([]string, 10)
Appending IDs adds them after those ten empty strings. Use length zero and capacity ten instead:
imageList := make([]string, 0, 10)
Indexing an empty decoded array
This can panic if the API returns no auth entries:
authToken := keystone.Auth[0].Token.ID
Validate the length first:
if len(keystone.Auth) == 0 {
return "", fmt.Errorf("authentication response contains no auth entry")
}
authToken := keystone.Auth[0].Token.ID
Ignoring an error from an earlier call
After getImages, check its error before queueing images:
imageList, err := getImages(authToken)
if err != nil {
return
}
err = queueImages(hostname, imageList)
Otherwise, assigning err = queueImages(...) overwrites any failure from getImages.
Comparisons
| Situation | Result value on failure | Correct approach |
|---|---|---|
client.Do(req) cannot reach server | Usually nil response | Check err before using res |
os.Open(path) cannot open a file | Usually nil file | Check err before defer file.Close() |
json.Unmarshal receives invalid JSON | Target may be partially populated | Check err before trusting decoded data |
slice[0] on an empty slice | No error value is returned | Check len(slice) > 0 first |
| Technique | Best use | Notes |
|---|---|---|
if err != nil { return ... } | Expected operational failures | Standard Go error-handling pattern |
panic(...) | Broken program invariants or unrecoverable startup failures | Usually not appropriate for unavailable APIs |
defer resource.Close() | Cleanup after successful acquisition | Place it only after acquisition succeeds |
recover() | Rare boundary-level panic handling | Does not replace normal error checks |
Cheat Sheet
result, err := operation()
if err != nil {
return err
}
// result is safe to use here
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("read body: %w", err)
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("unexpected status: %s", res.Status)
}
// Empty slice with room for 10 values:
values := make([]string, 0, 10)
// Safe before accessing items[0]:
if len(items) == 0 {
return err
}
Rules:
- Check
errimmediately after a call that returns one. - Do not access a pointer result until its error has been checked.
- Defer cleanup only after the resource was acquired successfully.
- Check errors from body reads separately from request errors.
- Validate slice lengths before indexing.
FAQ
What causes a nil pointer dereference in Go?
It occurs when code accesses a field or method through a nil pointer, such as res.Body when res is nil.
Can http.Client.Do return both a response and an error?
For normal transport failures, treat a non-nil error as meaning the response must not be used. Check the error first. In Go's HTTP API, a non-nil response with an error is only relevant in limited redirect-related cases, and the response body is already closed.
Why does defer res.Body.Close() panic before the function returns?
The expression res.Body.Close() must be evaluated when the defer statement is encountered. Accessing res.Body therefore panics immediately if res is nil.
Should I close an HTTP response body in Go?
Yes. Once client.Do succeeds, use defer res.Body.Close() to release associated resources.
Does an HTTP 404 or 500 make client.Do return an error?
No. A server response with status 404 or 500 generally produces a non-nil response and a nil transport error. Check res.StatusCode separately.
Why is make([]string, 10) a problem when collecting values?
It creates a slice that already contains ten empty strings. Use make([]string, 0, 10) when you want an empty slice with capacity for ten entries.
How should Go errors be wrapped?
Use %w when adding context to an error:
return fmt.Errorf("get token: %w", err)
This preserves the original error for inspection.
Mini Project
Description
Build a small Go function that fetches a JSON list of tasks from an HTTP endpoint. It demonstrates safe request creation, HTTP error handling, response-body cleanup, status validation, JSON decoding, and checking data before use.
Goal
Return task titles from a JSON API without panicking when the server is unavailable, returns an error status, or sends invalid data.
Requirements
Create a GET request to a configurable URL. Return a descriptive error if the request cannot be sent. Close the response body only after a successful request. Reject non-2xx HTTP status codes. Decode a JSON array and return the task titles. Use an HTTP client with a timeout.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.