Guide
Errors
Return HTTP errors from handlers, attach messages and metadata, and shape every error response in one place.
Handlers and middleware fail by returning an error. Zinc sends every returned error to one error handler, which turns it into a response. Handlers stay short, and error responses stay consistent across the whole app.
app.Get("/users/{id}", func(c *zinc.Context) error { user, err := users.Find(c.Context(), c.Param("id")) if errors.Is(err, ErrUserNotFound) { return zinc.ErrNotFound.WithMessage("user not found") } if err != nil { return err } return c.JSON(user)})What the default handler sends
Section titled “What the default handler sends”| Returned error | Response |
|---|---|
A Zinc HTTP error, such as zinc.ErrNotFound |
Its status code, with its message as a plain-text body |
| An error that wraps a Zinc HTTP error | The same, found with errors.As |
*zinc.BindError without an HTTP cause |
400 Bad Request, without decoder details |
| Any other error | 500 Internal Server Error, without the error text |
Unknown errors never leak their message to clients, so returning err straight from a database call is safe. It is just not very informative. Log it, or map it in a custom handler.
HTTP errors
Section titled “HTTP errors”Zinc predefines an error for each HTTP status: zinc.ErrBadRequest, zinc.ErrUnauthorized, zinc.ErrForbidden, zinc.ErrNotFound, zinc.ErrConflict, zinc.ErrUnprocessableEntity, zinc.ErrTooManyRequests, zinc.ErrInternalServerError, and the rest. Create one for any code with zinc.NewError(code).
Each builder method returns a copy, so the shared values are never modified:
return zinc.ErrBadRequest. WithMessage("email is invalid"). // client-facing text WithCause(err). // the underlying error, for logs; never sent WithMeta("field", "email"). // extra data for a custom handler WithHeader("X-Error-Code", "E1042") // a response headerThe default handler sends the message and headers. Meta and Cause are for your own handler and logs.
Stopping a middleware chain
Section titled “Stopping a middleware chain”Middleware can stop a request by returning an error instead of calling c.Next():
func requireAPIKey(c *zinc.Context) error { if c.GetHeader("X-API-Key") == "" { return zinc.ErrUnauthorized } return c.Next()}c.AbortWithStatus(code) is shorthand for returning zinc.NewError(code). To stop with a body rather than an error, write the response and return its result: return c.AbortWithJSON(zinc.StatusForbidden, body).
A custom error handler
Section titled “A custom error handler”Replace the default handler to send JSON, log failures, or report them to an error tracker. This one gives every error the same envelope:
cfg := zinc.DefaultConfigcfg.ErrorHandler = func(c *zinc.Context, err error) { status, message := zinc.StatusInternalServerError, "internal server error"
var httpErr *zinc.HTTPError if errors.As(err, &httpErr) { status, message = httpErr.Code, httpErr.Error() } else { slog.Error("request failed", "path", c.Path(), "err", err) }
_ = c.Status(status).JSON(zinc.Map{"error": message})}app := zinc.NewWithConfig(cfg)curl -i http://localhost:8080/users/nope# HTTP/1.1 404 Not Found# Content-Type: application/json; charset=utf-8## {"error":"user not found"}Map binding errors to 400
Section titled “Map binding errors to 400”The default handler maps *zinc.BindError to a generic 400 Bad Request, while preserving HTTP causes such as a body limit (413). Known JSON syntax/type errors are binding errors; opaque codec failures and invalid JSON destinations remain internal errors (500). Customize the response format once in your error handler:
cfg.ErrorHandler = func(c *zinc.Context, err error) { status, message := zinc.StatusInternalServerError, "internal server error"
var httpErr *zinc.HTTPError var bindErr *zinc.BindError switch { case errors.As(err, &httpErr): // check first: an oversized body is a BindError wrapping a 413 status, message = httpErr.Code, httpErr.Error() case errors.As(err, &bindErr): status, message = zinc.StatusBadRequest, "invalid "+bindErr.Source default: slog.Error("request failed", "path", c.Path(), "err", err) }
_ = c.Status(status).JSON(zinc.Map{"error": message})}Panics
Section titled “Panics”Without help, Go’s HTTP server catches a handler panic, logs it, and drops the connection, so the client gets no response at all. Add Recover early in the middleware chain, and panics become 500 responses that flow through your error handler like any other error.
Next steps
Section titled “Next steps”- Binding for the errors that binding and validation return.
- Recover for turning panics into errors.
- Errors API for the full
HTTPErrortype.
HTTP error modifiers copy the error. errors.Is(derived, zinc.ErrUnauthorized) does not identify a derived copy by its original sentinel. Use errors.As and inspect HTTPError.Code, or match an application cause attached with WithCause. Validation errors are application-defined: return an HTTP error from the validator or map your validation type in a custom error handler.