Plinth's typed error vocabulary. Six sentinel categories, factory helpers, and an HTTP middleware that maps any of them into RFC 7807 application/problem+json.
Design rationale: https://plinth.run/sdk/go/errors/.
go get github.com/plinth-dev/sdk-go/errors@latestThe package is named apperrors (not errors) to avoid colliding with the standard library. Standard import alias:
import apperrors "github.com/plinth-dev/sdk-go/errors"package items
import (
"context"
apperrors "github.com/plinth-dev/sdk-go/errors"
)
func (s *Service) Get(ctx context.Context, id string) (*Item, error) {
item, err := s.repo.Get(ctx, id)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, apperrors.NotFound("item", id) // 404 + not_found code
}
return nil, apperrors.Wrap(err, apperrors.CodeInternal, "items.get failed")
}
return item, nil
}In the HTTP layer, mount the middleware once and call apperrors.SetError from handlers:
mux := chi.NewRouter()
mux.Use(func(next http.Handler) http.Handler {
return apperrors.HTTPMiddleware(next)
})
mux.Get("/items/{id}", func(w http.ResponseWriter, r *http.Request) {
item, err := svc.Get(r.Context(), chi.URLParam(r, "id"))
if err != nil {
apperrors.SetError(r, err)
return
}
json.NewEncoder(w).Encode(item)
})A GET /items/missing request gets back:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://plinth.run/errors/not_found",
"title": "Not found",
"status": 404,
"detail": "item missing not found",
"code": "not_found"
}| Symbol | Purpose |
|---|---|
Err{NotFound,Conflict,PermissionDenied,Validation,Unauthenticated,Internal} |
Sentinels — match via errors.Is. |
Code{NotFound,…} |
Wire-stable string label. Maps 1:1 to a sentinel and HTTP status. |
*AppError |
Concrete struct with Code, Message, Fields, and HTTPStatus(). |
NotFound(resource, id) / Conflict(fmt, ...) / PermissionDenied(action) / Validation(msg, fields) / Unauthenticated(msg) / Internal(fmt, ...) |
Factory helpers. |
Wrap(cause, code, fmt, ...) |
Wrap a lower-level error; errors.Is works for both Code-sentinel and the wrapped cause. |
HTTPMiddleware(next, opts...) |
net/http middleware that reads SetError from the request context and emits problem+json. |
SetError(r, err) / GetError(r) / WriteProblem(w, r, err, opts...) |
The plumbing for the middleware path, plus a standalone escape hatch. |
WithLogger(l) / WithTypeURIPrefix(s) / WithTraceIDFunc(fn) |
Middleware options. |
- Internal errors are sanitized on the wire.
Code == CodeInternalhas itsMessagereplaced with"an internal error occurred"before serialization. The full original is logged atslog.Errorwith the trace ID for ops correlation. - Trace correlation is opt-in. Pass
WithTraceIDFuncto plug in your tracing system (typically OpenTelemetry); without it,Problem.TraceIDis empty. - The middleware does not overwrite a handler that already wrote a response. If the handler called
w.WriteandSetError, the response wins. Buggy handlers don't get clobbered.
- Go 1.23+ (uses
log/slog). - No external dependencies. Pure standard library.
- Semver from v0.1.0. Code constant string values are part of the public contract; changing one is a major-version bump.
MIT — see LICENSE.