Skip to content

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, HTTPStatus

A Problem has five standard fields from RFC 9457:

FieldTypeDescription
TypestringA URI identifying the error type. Defaults to "about:blank".
TitlestringA short, human-readable summary.
DetailstringA longer explanation specific to this occurrence.
StatusintThe HTTP status code. Defaults to 500.
InstancestringA URI identifying this specific occurrence.

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.

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)

Problems are immutable. The builder methods return new instances:

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.

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.

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.

When ServeHTTP is called, the response format is determined by the Accept header:

Accept HeaderResponse
application/problem+jsonProblem Details JSON
application/jsonProblem Details JSON
Anything else / missingPlain 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"
}

The Defaulted method fills in missing fields with sensible defaults from the request:

  • Type defaults to "about:blank".
  • Status defaults to 500.
  • Title defaults to Go’s http.StatusText() for the status code.
  • Instance defaults 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.

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.

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.