gohugoio/hugo · error
circular call stack detected in partial %q
Error message
circular call stack detected in partial %q
What it means
Raised by `partialCached` (tpl/partials/partials.go:223) when the partial being invoked, with the same cache-variant key, is already on the current template call stack. Because `partialCached` uses a get-or-create cache keyed by name+variants, a self-recursive cached call would block waiting on its own in-flight cache entry, so Hugo detects the cycle by walking the parent template chain and fails instead of deadlocking.
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 = depsManagerSharedView on GitHub ↗ (pinned to 8a468df065)
Solutions
- Use plain `partial` instead of `partialCached` for recursive partials.
- If caching is needed, pass a variant argument that differs at each recursion level (e.g. the section path or depth): `{{ partialCached "menu.html" . .Depth }}`.
- Break the cycle by splitting the partial: a cached outer wrapper that calls an uncached recursive inner partial.
Example fix
<!-- before: menu.html -->
{{ partialCached "menu.html" .Child }}
<!-- after -->
{{ partial "menu.html" .Child }} Defensive patterns
Strategy: validation
Validate before calling
{{/* break recursion with an explicit depth counter passed in context */}}
{{ $depth := default 0 .depth }}
{{ if lt $depth 10 }}
{{ partial "tree.html" (dict "node" .node "depth" (add $depth 1)) }}
{{ end }} Prevention
- Give recursive partials an explicit termination condition (depth counter or leaf check)
- Vary the context argument on each recursive call — identical name+context is detected as a cycle
- Prefer partialCached only for non-recursive partials; caching does not fix cycles
- Diagram partial include chains when A includes B includes A indirectly
When it happens
Trigger: A partial calling itself via `partialCached` (directly or through intermediaries) with identical variant arguments, e.g. `layouts/_partials/menu.html` containing `{{ partialCached "menu.html" . }}` for nested menu levels without a distinguishing variant.
Common situations: Recursive menu/tree rendering converted from `partial` to `partialCached` for performance; two partials that cachedly include each other; recursion where the variant key doesn't change per level so every recursion maps to the same cache entry.
Related errors
- maximum template call stack size exceeded in %q
- templates.Defer cannot be used inside a partialCached partia
- partial %q not found
- inner cannot be used inside a with block that wraps a partia
- can't apply myself (no turtles allowed)
AI-assisted analysis of gohugoio/hugo@8a468df065 (2026-07-31).
Data as JSON: /data/errors/d0284b89f20b579e.json.
Report an issue: GitHub ↗.