Pular para o conteúdo
Esta página ainda não foi traduzida. O conteúdo abaixo está em inglês. Ler em inglês

The basics

Error handling

Handlers return errors; one central handler negotiates HTML pages, htmx fragments or RFC 9457 problem details.

Última atualização:

Handlers never write partial error responses. They return an error, and the error handler renders the right response for the client, logs it and keeps internals out of client output.

Go
func (p *PostController) Show(c *web.Ctx) error {
	post, err := p.posts.Find(c.Context(), c.Param("post"))
	if errors.Is(err, store.ErrMissing) {
		return errs.ErrNotFound
	}
	if err != nil {
		return fmt.Errorf("find post: %w", err) // 500, cause logged, never shown
	}
	return c.JSON(http.StatusOK, post)
}

The errs package#

HTTPError#

Go
type HTTPError struct {
	Status  int         // HTTP status
	Message string      // safe to show to clients; empty means the status text
	Err     error       // internal cause: logged, never shown
	Headers http.Header // added to the response (Allow, Retry-After…)
	Key     string         // i18n key translating Message per request (errs.NewKey)
	Params  map[string]any // {name} placeholders of Key
}

errs.New(http.StatusConflict, "That slug is taken.")
errs.NewKey(http.StatusConflict, "posts.slug_taken", "That slug is taken.", nil) // translated in the request locale
errs.Wrap(http.StatusBadGateway, err)
errs.ErrTooManyRequests.WithHeader("Retry-After", "30")

Sentinel errors#

ErrBadRequest (400), ErrUnauthorized (401), ErrForbidden (403), ErrNotFound (404), ErrMethodNotAllowed (405), ErrConflict (409), ErrPreconditionFailed (412), ErrRequestTooLarge (413), ErrUnsupportedMediaType (415), ErrUnprocessable (422), ErrTooManyRequests (429), ErrInternal (500).

errors.Is matches any *HTTPError with the same status, even when wrapped or re-created with a cause:

Go
errors.Is(errs.Wrap(404, sql.ErrNoRows), errs.ErrNotFound) // true

ValidationError#

Go
verr := errs.NewValidation().
	Add("email", "The email is invalid.").
	Add("password", "The password must be at least 12 characters.")

verr.First("email") // "The email is invalid."
verr.Empty()        // false
return verr         // 422

Add returns the error for chaining, so errcheck flags it as a bare statement. Use Append (no result) when collecting messages in a loop or conditional:

Go
verr := errs.NewValidation()
if locked {
	verr.Append("price", "The price cannot change after the first sale.")
}
if !verr.Empty() {
	return verr
}

Status mapping#

errs.Status(err) returns the status of an *HTTPError, 422 for a *ValidationError, the status of any error implementing errs.StatusCoder (HTTPStatus() int), and 500 otherwise.

Content negotiation#

web.DefaultErrorHandler renders by client:

ClientResponse
Wants JSON (Accept has JSON and not HTML, or a JSON body with no preference)application/problem+json; 422 includes the errors map
Classic form post with a validation errorValidation hook: flash errors and redirect back (installed by the session package); GET/HEAD requests get the 422 page instead
htmx (HX-Request: true)HTML fragment with the status (htmx 4 swaps 4xx)
BrowserHTML page: the view errors/<status> when a Renderer has it, else a built-in page
404 problem+json
{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "instance": "/api/posts/99",
  "request_id": "fJ3k…"
}

Services.ProblemBaseURL prefixes the validation problem type URI (<base>/validation); otherwise it is about:blank.

What clients see#

  • 4xx: the HTTPError message, translated from its Key in the request locale when the app uses localization.
  • 5xx: only the status text, unless Services.Debug is set (never in production).
  • Every error is logged with method, path, status and request id: 5xx at ERROR, the rest at DEBUG.
  • If the response was already written, the error is only logged.

Custom handler#

Go
services := &web.Services{
	Errors: func(c *web.Ctx, err error) {
		if errors.Is(err, billing.ErrPaymentRequired) {
			_ = c.Problem(http.StatusPaymentRequired, "Payment required", "Upgrade your plan.")
			return
		}
		web.DefaultErrorHandler(c, err)
	},
}

Middleware renders errors with the same handler through web.Fail(w, r, err).

Panics#

middleware.Recover() converts panics to 500 responses through the error handler and logs the stack trace server-side only.

Database errors#

orm.ErrNotFound (returned by First and Find) matches errs.ErrNotFound, so returning it answers 404. Unique-constraint violations are wrapped in orm.ErrConflict and answer 409.

Debug error page#

With APP_DEBUG=true outside production, 5xx errors in a browser render a debug page: the error message and root type, the Unwrap chain, the stack with source snippets (app frames open, framework and standard library frames collapsed), the request (method, URL, route, request id, headers with Authorization, Cookie and token headers redacted) and the Go version. Stacks come from recovered panics and from errors created with errs.New, Wrap or WithStack. JSON clients keep getting problem+json (with the error text as detail) and htmx gets the fragment. vukem serve prints the same report in colour on stderr. The page never renders in production.