gohugoio/hugo · error

circular call stack detected in partial

Error message

circular call stack detected in partial %q

What it means

Thrown by IncludeCached when walking the CurrentTemplate chain reveals that the same partialCached with the same cache key is already an ancestor in the call stack. Continuing would deadlock because GetOrCreate on lazycache blocks on an in-progress entry.

Solutions

  1. Use plain partial (not partialCached) for the recursive call so the cache is not involved.
  2. Break the cycle by splitting the recursive part into a separate non-cached partial.
  3. Ensure the variants differentiate recursive invocations if caching is genuinely needed per level.
  4. Render tree structures with a different pattern (e.g. a single partial iterating over children) instead of recursive cached calls.

Example fix

// before (recursive cached self-call):
{{ partialCached "menu.html" . }}
// after (use plain partial for recursion):
{{ partial "menu.html" . }}
Defensive patterns

Strategy: validation

Validate before calling

{{ if not (eq $.PartialName "menu.html") }}
  {{ partialCached "menu.html" . }}
{{ else }}
  {{ partial "menu.html" . }}
{{ end }}

Prevention

When it happens

Trigger: A partial invoked via partialCached calls itself (directly or via another partialCached) with the same variants, creating a cycle: partial A is cached and calls partialCached A. Detected by comparing parent.CurrentTemplateInfoOps and parent.Key against the current invocation.

Common situations: Recursive partials (e.g. rendering a tree/menu where a node partial calls partialCached on child nodes with the same name), refactoring a partial to use partialCached without removing a self-reference, or variant values that collide to the same cache key.

Related errors


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

Appendix: source

Thrown at tpl/partials/partials.go:223

func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any, variants ...any) (any, error) {
	start := time.Now()
	key := partialCacheKey{
		Name:     name,
		Variants: variants,
	}
	keyString := key.Key()

	depsManagerIn := tpl.Context.GetDependencyManagerInCurrentScope(ctx)
	ti, err := ns.lookup(name)
	if err != nil {
		return nil, err
	}

	if parent := tpl.Context.CurrentTemplate.Get(ctx); parent != nil {
		for parent != nil {
			if parent.CurrentTemplateInfoOps == ti && parent.Key == keyString {
				// This will deadlock if we continue.
				return nil, fmt.Errorf("circular call stack detected in partial %q", ti.Filename())
			}
			parent = parent.Parent
		}
	}

	r, found, err := ns.cachedPartials.cache.GetOrCreate(keyString, func(string) (includeResult, error) {
		var depsManagerShared identity.Manager
		if ns.deps.Conf.Watching() {
			// We need to create a shared dependency manager to pass downwards
			// and add those same dependencies to any cached invocation of this partial.
			depsManagerShared = identity.NewManager()
			ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, depsManagerShared.(identity.DependencyManagerScopedProvider))
		}
		// Mark the ctx so templates.Defer can reject being called from a cached body.
		ctx = tpl.Context.IsInPartialCached.Set(ctx, true)
		r := ns.doInclude(ctx, keyString, ti, context)
		if ns.deps.Conf.Watching() {
			r.mangager = depsManagerShared

View on GitHub (pinned to 52c9bd7908)