gohugoio/hugo · error

is an unexported method of type

Error message

%s is an unexported method of type %s

What it means

Thrown by Hugo's `where` template function while resolving the key against each element. `evaluateSubElem` (tpl/collections/where.go:328-341) first looks up a method by name; if it finds one but the method's reflect PkgPath is non-empty (i.e. its first letter is lowercase / unexported), it refuses to call it. Go's reflect package cannot invoke unexported methods, so Hugo surfaces the name and type so you can correct the template key.

Solutions

  1. Use the exported (capitalized) method name as the where key, e.g. `CountItems` instead of `countItems`.
  2. If no exported equivalent exists, add a thin exported accessor method on the element type.
  3. Verify the method name against the type with `reflect.TypeFor[T]().Method(i).Name` or the package docs.

Example fix

// before
{{ where $items "countItems" "==" 3 }}
// after
{{ where $items "CountItems" "==" 3 }}
Defensive patterns

Strategy: validation

Validate before calling

{{/* Verify the name resolves to an exported method before filtering */}}
{{ $t := reflect.TypeOf (index $items 0) }}
{{ $ok := false }}
{{ range $i := seq (len $t.NumMethod false) }}
  {{ $m := $t.Method $i }}
  {{ if eq $m.Name $key }}{{ $ok = true }}{{ end }}
{{ end }}

Prevention

When it happens

Trigger: Calling `{{ where $items "privateHelper" "==" x }}` where the element type has a method `func (t) privateHelper() ...` (lowercase). Also reachable via a dotted key path like `.foo.privateHelper` where a path segment names an unexported method.

Common situations: Template author guesses a method name by guessing lowercase Go conventions; an embedded/internal type exposes only a private helper that the public API wraps; refactoring renames a method to lowercase.

Related errors


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

Appendix: source

Thrown at tpl/collections/where.go:341

	objPtr := obj
	if !hreflect.IsInterfaceOrPointer(objPtr.Kind()) && objPtr.CanAddr() {
		objPtr = objPtr.Addr()
	}

	mt := hreflect.GetMethodByNameForType(objPtr.Type(), elemName)
	if mt.Func.IsValid() {
		// Receiver is the first argument.
		args := []reflect.Value{objPtr}
		num := mt.Type.NumIn()
		maxNumIn := 1
		if num > 1 && hreflect.IsContextType(mt.Type.In(1)) {
			args = append(args, ctx)
			maxNumIn = 2
		}

		switch {
		case mt.PkgPath != "":
			return zero, fmt.Errorf("%s is an unexported method of type %s", elemName, typ)
		case mt.Type.NumIn() > maxNumIn:
			return zero, fmt.Errorf("%s is a method of type %s but requires more than %d parameter", elemName, typ, maxNumIn)
		case mt.Type.NumOut() == 0:
			return zero, fmt.Errorf("%s is a method of type %s but returns no output", elemName, typ)
		case mt.Type.NumOut() > 2:
			return zero, fmt.Errorf("%s is a method of type %s but returns more than 2 outputs", elemName, typ)
		case mt.Type.NumOut() == 1 && mt.Type.Out(0).Implements(errorType):
			return zero, fmt.Errorf("%s is a method of type %s but only returns an error type", elemName, typ)
		case mt.Type.NumOut() == 2 && !mt.Type.Out(1).Implements(errorType):
			return zero, fmt.Errorf("%s is a method of type %s returning two values but the second value is not an error type", elemName, typ)
		}
		res := mt.Func.Call(args)
		if len(res) == 2 && !res[1].IsNil() {
			return zero, fmt.Errorf("error at calling a method %s of type %s: %s", elemName, typ, res[1].Interface().(error))
		}
		return res[0], nil
	}

View on GitHub (pinned to 52c9bd7908)