gohugoio/hugo · error

stopLevel: %w

Error message

stopLevel: %w

What it means

Returned by Fragments.ToHTML when cast.ToIntE(stopLevel) fails: the stopLevel argument is not convertible to int. Identical shape to the startLevel error but for the stop/end level. Uses %w so the underlying cast error is unwrappable. Note a stopLevel of -1 is a valid sentinel meaning 'include all'.

Source

Thrown at markup/tableofcontents/tableofcontents.go:163

		heading = heading.Headings[len(heading.Headings)-1]
	}
	heading.Headings = append(heading.Headings, h)
}

// ToHTML renders the ToC as HTML.
func (toc *Fragments) ToHTML(startLevel, stopLevel any, ordered bool) (template.HTML, error) {
	if toc == nil {
		return "", nil
	}

	iStartLevel, err := cast.ToIntE(startLevel)
	if err != nil {
		return "", fmt.Errorf("startLevel: %w", err)
	}

	iStopLevel, err := cast.ToIntE(stopLevel)
	if err != nil {
		return "", fmt.Errorf("stopLevel: %w", err)
	}

	b := &tocBuilder{
		s:          strings.Builder{},
		h:          toc.Headings,
		startLevel: iStartLevel,
		stopLevel:  iStopLevel,
		ordered:    ordered,
	}
	b.Build()
	return template.HTML(b.s.String()), nil
}

func (toc Fragments) walk(fn func(*Heading)) {
	for _, h := range toc.Headings {
		h.walk(fn)
	}
}

View on GitHub (pinned to 52c9bd7908)

Solutions

  1. Pass stopLevel as an int (use -1 to include all levels).
  2. Coerce the source value: {{ $stop := int (.Params.stop_level | default 3) }}.
  3. Double-check argument order: ToHTML(startLevel, stopLevel, ordered).

Example fix

{{/* before */}}
{{ .Fragments.ToHTML 2 .Params.end_level true }}

{{/* after */}}
{{ .Fragments.ToHTML 2 (int (.Params.end_level | default 3)) true }}
Defensive patterns

Strategy: type-guard

Validate before calling

// Coerce stopLevel; -1 means include all.
func toStopLevel(v any, def int) int {
    switch t := v.(type) {
    case int:
        return t
    case float64:
        return int(t)
    case string:
        if n, err := strconv.Atoi(t); err == nil {
            return n
        }
    }
    return def
}

Type guard

func isIntLike(v any) bool {
    switch v.(type) {
    case int, int64, float64:
        return true
    case string:
        _, err := strconv.Atoi(v.(string))
        return err == nil
    }
    return false
}

Try / catch

html, err := frags.ToHTML(start, stop, ordered)
if err != nil && strings.HasPrefix(err.Error(), "stopLevel:") {
    html, err = frags.ToHTML(start, 3, ordered)
}

Prevention

When it happens

Trigger: Calling Fragments.ToHTML(start, stop, ordered) with stop set to a non-integer string, e.g. ToHTML(2, "3", false) where "3" is a string, or ToHTML(2, nil, false). Fires from a layout partial rendering the ToC.

Common situations: Template params read from front matter/section params as strings; a partial receiving a shortcode arg as string; passing .Site.Params.toc.end_level without conversion; accidentally passing an ordered bool into the stopLevel slot.

Related errors


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