gohugoio/hugo · error
%v
Error message
%v
What it means
Hugo adds a 'try' keyword to templates. When a function or method invoked under try panics, a deferred recover in evalCall wraps the value into a TryValue with an Err. This exact %v format string fires only when the recovered panic value is NOT an error type (e.g. panic with a string or int), so it is stringified generically. See hugo_template.go:296-307.
Source
Thrown at tpl/internal/go_templates/texttemplate/hugo_template.go:303
// Err is the error returned by the function or method wrapped with "try".
// This will always be nil if Value is set.
Err *TryError
}
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
// as the function itself.
func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node, name string, args []parse.Node, final reflect.Value, first ...reflect.Value) (val reflect.Value) {
// Added for Hugo.
if name == "try" {
defer func() {
if r := recover(); r != nil {
// Cause: herrors.Cause(err)
if err, ok := r.(error); ok {
val = reflect.ValueOf(TryValue{Value: nil, Err: newErrorWithCause(err)})
} else {
val = reflect.ValueOf(TryValue{Value: nil, Err: newErrorWithCause(fmt.Errorf("%v", r))})
}
}
}()
}
if args != nil {
args = args[1:] // Zeroth arg is function name/node; not passed to function.
}
typ := fun.Type()
numFirst := len(first) // Added for Hugo
numIn := len(args) + numFirst // Added for Hugo
if !isMissing(final) {
numIn++
}
numFixed := len(args) + len(first) // Adjusted for Hugo
if typ.IsVariadic() {
numFixed = typ.NumIn() - 1 // last arg is the variadic one.
if numIn < numFixed {View on GitHub (pinned to 52c9bd7908)
Solutions
- Make the panicking function return an error instead of panicking.
- If it must panic, panic with an error value so the cause chain and TryError.Cause are preserved.
- Inspect the TryValue.Err to handle the failure in the template rather than letting it propagate.
Example fix
// before
func badFunc() string { panic("nope") }
// after
func badFunc() (string, error) { return "", errors.New("nope") } Defensive patterns
Strategy: try-catch
Type guard
// Narrow a TryValue returned by the 'try' template keyword.
{{ with $tv := try $fn }}
{{ with $tv.Err }}ERR: {{ . }}{{ else }}OK: {{ $tv.Value }}{{ end }}
{{ end }} Try / catch
func recoverSafely(fn func() (any, error)) (v any, err error) {
defer func() {
if r := recover(); r != nil {
if e, ok := r.(error); ok {
err = e
} else {
err = fmt.Errorf("%v", r)
}
}
}()
return fn()
} Prevention
- Prefer returning errors over panicking in functions exposed to templates.
- When panicking is unavoidable, panic with an error value so the cause chain survives.
When it happens
Trigger: {{ try (someFunc) }} where someFunc does panic("boom") (a string) or panic(42) instead of returning an error or panicking with an error value.
Common situations: A custom Hugo template func/method that panics with a raw string rather than an error; a nil-pointer dereference inside a try block whose recovered runtime.Error is re-wrapped here.
Related errors
- can't divide the value by 0
- no alias template found
- want 1 or 2 arguments
- first argument must be a map
- text is already rendered, repeating it may cause infinite re
AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09).
Data as JSON: /api/errors/0700ba3e6ccffd20.
Report an issue: GitHub.