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
- Use plain partial (not partialCached) for the recursive call so the cache is not involved.
- Break the cycle by splitting the recursive part into a separate non-cached partial.
- Ensure the variants differentiate recursive invocations if caching is genuinely needed per level.
- 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
- Never call partialCached from within the partial it caches.
- Use plain partial for recursive structures (trees, menus).
- Give cached variants distinct values so cache keys differ across recursion levels.
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
- can't apply myself (no turtles allowed)
- could not find in the import context
- failed to transform internal partial decorator template
- maximum template call stack size exceeded in
- partial not found
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 = depsManagerSharedView on GitHub (pinned to 52c9bd7908)