gohugoio/hugo · error

elements must be comparable

Error message

elements must be comparable

What it means

Hugo's `intersect`, `union`, and `symDiff` functions internally call `collectIdentities`, which builds a set of element identities from the input slices. Go's `reflect.Type.Comparable()` returns false for slices, maps, and funcs, so this error fires when those non-comparable types appear as elements. Numeric types are normalized and non-comparable structs are hashed, but raw slices/maps/funcs cannot be placed in a map key and are rejected.

Solutions

  1. Convert elements to a comparable scalar (e.g. extract an `.RelPermalink` or `.Title` string) before intersecting.
  2. Use `collections.SymDiff`/`Intersect` only on slices of strings, numbers, or comparable structs.
  3. If comparing maps, build a slice of a comparable key field and operate on that.

Example fix

// before
{{ intersect $pageMaps1 $pageMaps2 }}
// after
{{ $keys1 := apply $pageMaps1 "index" "." "Title" }}
{{ $keys2 := apply $pageMaps2 "index" "." "Title" }}
{{ intersect $keys1 $keys2 }}
Defensive patterns

Strategy: type-guard

Type guard

func elementsComparable(s any) bool {
    v := reflect.ValueOf(s)
    if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { return false }
    elem := v.Type().Elem()
    if elem.Kind() == reflect.Pointer { elem = elem.Elem() }
    return elem.Comparable()
}

Prevention

When it happens

Trigger: Calling `{{ intersect $a $b }}` (or `union`/`symDiff`) where one or both slices contain elements of a non-comparable type — e.g. a `[][]int`, a slice of maps, or a slice containing func values. The check at reflect_helpers.go:66–67 fires per element.

Common situations: A developer tries to intersect two collections of page Params (maps) or nested slices, expecting deep comparison. This is unsupported because Go maps and slices are not usable as map keys.

Related errors


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

Appendix: source

Thrown at tpl/collections/reflect_helpers.go:67

		return ip.TransientKey()
	}

	return vv
}

// collects identities from the slices in seqs into a set. Numeric values are normalized,
// pointers unwrapped.
func collectIdentities(seqs ...any) (map[any]bool, error) {
	seen := make(map[any]bool)
	for _, seq := range seqs {
		v := reflect.ValueOf(seq)
		switch v.Kind() {
		case reflect.Array, reflect.Slice:
			for i := range v.Len() {
				ev, _ := hreflect.Indirect(v.Index(i))

				if !ev.Type().Comparable() {
					return nil, errors.New("elements must be comparable")
				}

				seen[normalize(ev)] = true
			}
		default:
			return nil, fmt.Errorf("arguments must be slices or arrays")
		}
	}

	return seen, nil
}

// We have some different numeric and string types that we try to behave like
// they were the same.
func convertValue(v reflect.Value, to reflect.Type) (reflect.Value, error) {
	if v.Type().AssignableTo(to) {
		return v, nil
	}

View on GitHub (pinned to 52c9bd7908)