gohugoio/hugo · error

uneven number of replacement pairs

Error message

uneven number of replacement pairs

What it means

`strings.ReplacePairs` builds a `strings.Replacer` from the collected pair list, which requires old/new values in matched pairs — an odd count means some old value has no replacement. Hugo validates `len(p)%2 != 0` before constructing the Replacer, since `strings.NewReplacer` would otherwise panic.

Source

Thrown at tpl/strings/strings.go:301

	}
	if p == nil {
		// inline form: ReplacePairs "a" "b" "s"
		p = make([]string, len(args)-1)
		for i, v := range args[:len(args)-1] {
			s, err := cast.ToStringE(v)
			if err != nil {
				return "", err
			}
			p[i] = s
		}
	}

	if len(p) == 0 || ss == "" {
		return ss, nil
	}

	if len(p)%2 != 0 {
		return "", fmt.Errorf("uneven number of replacement pairs")
	}

	key := strings.Join(p, "\x00")
	replacer, err := ns.replacerCache.GetOrCreate(key, func() (*strings.Replacer, error) {
		return strings.NewReplacer(p...), nil
	})
	if err != nil {
		return "", err
	}

	return replacer.Replace(ss), nil
}

// SliceString slices a string by specifying a half-open range with
// two indices, start and end. 1 and 4 creates a slice including elements 1 through 3.
// The end index can be omitted, it defaults to the string's length.
func (ns *Namespace) SliceString(a any, startEnd ...any) (string, error) {
	aStr, err := cast.ToStringE(a)

View on GitHub ↗ (pinned to 8a468df065)

Solutions

  1. Count the pair elements — every old string needs a new string; add the missing element.
  2. Check you didn't omit the trailing source argument: the last argument is always the source, not part of a pair.
  3. If pairs come from a data file, validate the list length is even at the source.

Example fix

<!-- before -->
{{ strings.ReplacePairs (slice "a" "A" "b") $src }}
<!-- after -->
{{ strings.ReplacePairs (slice "a" "A" "b" "B") $src }}
Defensive patterns

Strategy: validation

Validate before calling

{{ if eq (mod (len $pairs) 2) 0 }}{{ strings.Replacer $s $pairs }}{{ else }}{{ errorf "replacement pairs uneven: %v" $pairs }}{{ end }}

Type guard

func evenPairs(pairs []string) bool { return len(pairs)%2 == 0 }

Try / catch

out, err := ns.Replacer(args...)
if err != nil {
    // odd pair count — one old is missing its new; fix config, don't drop the last element
}

Prevention

When it happens

Trigger: An odd number of pair elements: inline form `{{ strings.ReplacePairs "a" "b" "c" $src }}` (three pair args), or slice form `{{ strings.ReplacePairs (slice "a" "b" "c") $src }}`. Empty pairs skip this check (the source is returned unchanged), so only genuinely odd counts trigger it.

Common situations: Forgetting the source string goes last, so the last replacement value gets consumed as the source and the remaining pairs become odd; dropping one element when editing a YAML list of pairs in front matter; programmatically appending to the pairs slice and missing a value.

Related errors


AI-assisted analysis of gohugoio/hugo@8a468df065 (2026-07-31). Data as JSON: /data/errors/116be4c7f928532d.json. Report an issue: GitHub ↗.