Error Handling¶
Burrow provides a unified error handling system that renders styled error pages, supports JSON API responses, and integrates with the template and i18n systems. Error pages are fully customizable through templates.
How Errors Flow¶
When a handler returns an error, Handle() processes it:
r.Get("/notes/:id", burrow.Handle(func(w http.ResponseWriter, r *http.Request) error {
note, err := repo.Get(r.Context(), chi.URLParam(r, "id"))
if err != nil {
return burrow.NewHTTPError(http.StatusNotFound, "note not found")
}
return burrow.Render(w, r, http.StatusOK, "notes/detail", map[string]any{
"Note": note,
})
}))
The error handling chain:
*HTTPError—Handle()callsRenderError(w, r, code, message)*ValidationError— returned byBind()when struct validation fails (see below)- Any other error — logged as "unhandled error", rendered as 500
- Response already started — logged, no further action (can't change status code)
Errors with status code >= 500 are logged automatically. 4xx errors are not logged (they're expected client errors).
Validation Errors¶
burrow.Bind() parses and validates request bodies. When validation fails, it returns a *ValidationError containing per-field errors:
func createItem(w http.ResponseWriter, r *http.Request) error {
var input struct {
Name string `form:"name" validate:"required"`
Email string `form:"email" validate:"required,email"`
}
if err := burrow.Bind(r, &input); err != nil {
var ve *burrow.ValidationError
if errors.As(err, &ve) {
// ve.Errors contains []FieldError with Field and Message
return burrow.JSON(w, http.StatusUnprocessableEntity, ve.Errors)
}
return err // other parse errors become 500
}
// input is valid
return burrow.JSON(w, http.StatusOK, input)
}
ValidationError is not an HTTPError — it does not automatically render an error page. Your handler decides how to present validation failures (JSON response, re-render form with errors, etc.). See the Validation guide for form-based patterns with i18n.TranslateValidationErrors().
RenderError¶
RenderError picks the response format automatically:
- JSON API requests (
Accept: application/json) get a JSON response: - HTML requests render the
error/{code}template (e.g.error/404) through the standardRenderpipeline — with layout wrapping, HTMX fragment support, and i18n
You can also call RenderError directly in middleware:
func myMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !authorized(r) {
burrow.RenderError(w, r, http.StatusForbidden, "forbidden")
return
}
next.ServeHTTP(w, r)
})
}
Built-in Error Pages¶
Burrow ships default error templates for these status codes:
| Template | Status Code | Default Message |
|---|---|---|
error/403 |
403 | You do not have permission to access this page. |
error/404 |
404 | The page you are looking for does not exist. |
error/405 |
405 | The request method is not supported for this page. |
error/500 |
500 | An unexpected error occurred. Please try again later. |
The default templates are minimal HTML without any CSS framework. They are designed to be overridden.
Custom Error Pages¶
To provide your own error pages, define templates with the same names in your app's template FS. The last {{ define }} wins, so app templates override the built-in defaults:
{{ define "error/404" }}
<div class="container text-center py-5">
<h1 class="display-1">404</h1>
<p class="lead">{{ .Message }}</p>
<a href="/" class="btn btn-primary">Back to Home</a>
</div>
{{ end }}
Template data available:
| Key | Type | Description |
|---|---|---|
.Code |
int |
HTTP status code |
.Message |
string |
Translated error message |
Since error pages go through the standard Render pipeline, they are wrapped in your layout and have access to all template functions (navLinks, currentUser, csrfToken, t, lang, etc.).
Design System Integration¶
A design-system or shell app can override error templates to provide styled pages that match the rest of your application. The override chain is:
- Burrow core — minimal HTML (always present)
- Design system / shell app — styled with your CSS framework (e.g. a Tailwind-based
app/errortemplate) - Your app — fully custom (if you need per-app error pages)
Each layer overrides the previous one simply by defining the same template name — but the layer must be registered with NewServer after the layer it overrides, because Burrow parses templates in registration order and html/template's last-define-wins decides which definition is used. See Layouts → Overriding Contrib Templates for the full pattern.
i18n¶
Error messages are automatically translated using the error-{code} i18n key (e.g. error-404). Burrow ships translations for English and German. To add translations for other languages, include keys in your translation files:
# active.fr.toml
error-403 = "Vous n'avez pas la permission d'accéder à cette page."
error-404 = "La page que vous recherchez n'existe pas."
error-405 = "La méthode de requête n'est pas prise en charge pour cette page."
error-500 = "Une erreur inattendue s'est produite. Veuillez réessayer plus tard."
If a translation key is not found, RenderError falls back to the original English message passed by the handler.
Chi NotFound and MethodNotAllowed¶
Burrow automatically registers custom handlers for Chi's NotFound and MethodNotAllowed callbacks. Requests to undefined routes or with wrong HTTP methods render styled error pages instead of Chi's default plain-text responses.
This works because the handlers go through Handle(), which calls RenderError(), which renders the error template with your layout.