Example: HTTP Client Handle HTTP Client Errors
An HTTP client request can fail in two different ways:
- the request fails before an HTTP response is received;
- the server returns an HTTP response with an error status.
These cases are handled differently in Go.
Quick Example
Check the error from client.Do first, then inspect the HTTP status:
The important distinction is:
A 404 Not Found or 500 Internal Server Error normally produces a valid *http.Response with err == nil.
HTTP Status Is Not a Go Error
This does not automatically return an error:
if the server responds:
You get:
net/http does not treat HTTP status codes such as 400, 404, or 500 as Go errors.
Your application decides which status codes are acceptable.
For example:
For an API that accepts any successful 2xx response, checking the whole 2xx range is usually more appropriate:
Transport Errors
client.Do returns an error when the request cannot successfully obtain an HTTP response.
Common examples include:
- DNS lookup failure
- connection refused
- connection timeout
- TLS handshake failure
- request cancellation
- failure while following a redirect
For example:
There is no HTTP status code to inspect in this case.
Keep the original error with %w so callers can inspect it with errors.Is or errors.As.
Inspect Specific Errors
Don't parse error strings when the standard library provides a typed error.
For example, a timeout can be detected with:
A request canceled through its context can be checked with:
A deadline can be checked with:
This is more reliable than checking whether the error message contains "timeout" or "context canceled".
Check the Response Before Reading It
Once client.Do succeeds, always check the response before assuming it contains the data you expect:
Don't decode a known error response as if it were a successful API response.
For APIs that return structured error bodies, you may want to decode the error response separately.
Don't Lose the Response Body
An HTTP error status still gives you a response body.
For example, an API might return:
with:
If that information is useful, read it before returning:
For untrusted or potentially large responses, don't use an unlimited io.ReadAll. Apply an appropriate size limit.
Preserve the Original Error
Wrap errors with context:
The %w keeps the original error available:
Avoid converting every error into a plain string:
%v formats the error but does not preserve it for errors.Is and errors.As.
Use %w when the caller may need to inspect the underlying error.
Be Careful With Retries
Not every error should be retried.
A timeout while contacting a service may be transient. A 400 Bad Request usually indicates that retrying the identical request will not fix the problem.
Also consider whether the request can safely be repeated.
A GET is normally easier to retry than a request that creates a resource or triggers an external side effect.
Retry policy belongs above the basic HTTP error check. This example only determines what happened; it does not automatically retry the request.
A Useful Error Boundary
For application code, it is often useful to keep transport failures and HTTP status failures distinguishable:
Then:
Callers can inspect it without parsing an error string:
Whether you need a custom error type depends on the application's API. Don't introduce one just to wrap a single call site.
Production Notes
Always distinguish:
from:
The first is represented by a Go error. The second is represented by an HTTP response.
Close every response body.
Limit the amount of an error response that you read.
Wrap errors with %w when callers may need to inspect their causes.
Don't retry automatically just because client.Do returned an error. Check the error, request semantics, and retry policy first.
And don't turn every non-2xx response into a generic string too early. The status code and response body often contain information the caller needs.