gohugoio/hugo · error

startLevel

Error message

startLevel: %w

What it means

Returned by Fragments.ToHTML when cast.ToIntE(startLevel) fails, i.e. the startLevel argument cannot be converted to an int. startLevel is typed as `any`, so template authors can pass a non-numeric string and trigger this. The error wraps the cast error with %w so the original cast error is unwrappable.

Solutions

  1. Pass startLevel as an int literal or cast it: ToHTML (intVal, stop, ordered).
  2. If the value comes from params, coerce with int or a default: {{ $start := int (.Params.start_level | default 2) }}.
  3. Validate the param type in your template before calling ToHTML.

Example fix

{{/* before */}}
{{ .Fragments.ToHTML .Params.start_level .Params.stop_level false }}

{{/* after */}}
{{ .Fragments.ToHTML (int (.Params.start_level | default 2)) (int (.Params.stop_level | default 3)) false }}
Defensive patterns

Strategy: type-guard

Validate before calling

// Coerce before calling ToHTML.
func toIntLevel(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(), "startLevel:") {
    html, err = frags.ToHTML(2, stop, ordered) // sane default
}

Prevention

When it happens

Trigger: Calling .Fragments.ToHTML(...) (or the {{.TableOfContents}}-style path) with startLevel set to a non-integer string, e.g. ToHTML("abc", 3, false). Common via a template that interpolates a config value as a string instead of an int.

Common situations: A custom layout passing .Site.Params.toc.start_level (a string from front matter) straight into ToHTML; partials that receive parameters from a shortcode as strings; upgrading where an old template passed a stringified level.

Related errors


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

Appendix: source

Thrown at markup/tableofcontents/tableofcontents.go:158

	for i := 1; i < level; i++ {
		if len(heading.Headings) == 0 {
			heading.Headings = append(heading.Headings, &Heading{})
		}
		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
}

View on GitHub (pinned to 52c9bd7908)