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

  1. Make the panicking function return an error instead of panicking.
  2. If it must panic, panic with an error value so the cause chain and TryError.Cause are preserved.
  3. 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

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


AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09). Data as JSON: /api/errors/0700ba3e6ccffd20. Report an issue: GitHub.