Problem Details
The problem module implements RFC 9457 — Problem Details for HTTP APIs. It provides a Problem type that serves as both a Go error and an http.Handler, enabling structured error responses with content negotiation.
type Problem struct { Type string // URI identifying the error type (default: "about:blank") Title string // Short, human-readable summary Detail string // Longer explanation for this occurrence Status int // HTTP status code (default: 500) Instance string // URI identifying this specific occurrence}
// Implements: error, http.Handler, HTTPStatusThe Problem Type
Section titled “The Problem Type”A Problem has five standard fields from RFC 9457:
| Field | Type | Description |
|---|---|---|
Type | string | A URI identifying the error type. Defaults to "about:blank". |
Title | string | A short, human-readable summary. |
Detail | string | A longer explanation specific to this occurrence. |
Status | int | The HTTP status code. Defaults to 500. |
Instance | string | A URI identifying this specific occurrence. |
Using Problems as Errors
Section titled “Using Problems as Errors”Problem implements the error interface. Return it from handlers and the framework handles the rest:
func getUser(w http.ResponseWriter, r *http.Request) error { user, err := db.FindUser(r.Context(), r.PathValue("id")) if err != nil { return problem.Problem{ Title: "User Not Found", Detail: fmt.Sprintf("No user with ID %s", r.PathValue("id")), Status: http.StatusNotFound, } }
return response.JSON(w, http.StatusOK, user)}Since Problem also implements http.Handler, the framework calls ServeHTTP on it directly, which means the content negotiation and response formatting happen automatically.
Using Problems as Handlers
Section titled “Using Problems as Handlers”You can use a Problem directly as an HTTP handler anywhere a http.Handler is needed:
notFound := problem.Problem{ Title: "Not Found", Status: http.StatusNotFound,}
mux.Handle("/", notFound)Immutable Builder Pattern
Section titled “Immutable Builder Pattern”Problems are immutable. The builder methods return new instances:
With / Without
Section titled “With / Without”Add or remove additional fields (extension members in RFC 9457 terminology):
err := problem.Problem{ Title: "Validation Error", Status: http.StatusBadRequest,}
detailed := err. With("field", "email"). With("reason", "invalid format")
cleaned := detailed.Without("reason")Additional fields are merged into the top-level JSON output alongside the standard fields.
WithError
Section titled “WithError”Attach an underlying Go error for errors.Is() and errors.As() chains:
p := problem.Problem{ Title: "Database Error", Status: http.StatusInternalServerError,}.WithError(err)
// Later:if errors.Is(p, sql.ErrNoRows) { // works through Unwrap()}The wrapped error is available via Unwrap() but is never exposed in the HTTP response.
WithStackTrace
Section titled “WithStackTrace”Attach a stack trace for development debugging:
p := problem.Problem{ Title: "Unexpected Error", Status: http.StatusInternalServerError,}.WithStackTrace()Stack traces are only included in the response when using ServeHTTPDev (the development-mode handler). The standard ServeHTTP never exposes stack traces.
Content Negotiation
Section titled “Content Negotiation”When ServeHTTP is called, the response format is determined by the Accept header:
| Accept Header | Response |
|---|---|
application/problem+json | Problem Details JSON |
application/json | Problem Details JSON |
| Anything else / missing | Plain text fallback |
The JSON response includes all standard fields plus any additional fields set with With:
{ "type": "https://example.com/errors/not-found", "title": "Resource Not Found", "detail": "The user with ID 42 was not found.", "status": 404, "instance": "/users/42", "field": "email", "reason": "invalid format"}Defaults
Section titled “Defaults”The Defaulted method fills in missing fields with sensible defaults from the request:
Typedefaults to"about:blank".Statusdefaults to500.Titledefaults to Go’shttp.StatusText()for the status code.Instancedefaults to the request URL path.
The framework calls Defaulted automatically when creating Problem responses from handler errors. You generally don’t need to call it yourself.
HTTPStatus
Section titled “HTTPStatus”Problem implements the HTTPStatus() int method, which returns its Status field. This is how the framework extracts the correct HTTP status code from problem errors.
Sentinel Problems
Section titled “Sentinel Problems”Define reusable problem templates as package-level variables:
var ErrNotFound = problem.Problem{ Title: "Resource Not Found", Status: http.StatusNotFound,}
var ErrUnauthorized = problem.Problem{ Title: "Unauthorized", Detail: "Valid authentication credentials are required.", Status: http.StatusUnauthorized,}
// In a handler:return ErrNotFound.With("resource", "user").With("id", userId)Since the builder methods return new instances, the sentinels are never mutated.