spectreconsole/spectre.console · error · CircularTreeException
Cycle detected in tree - unable to render.
Error message
Cycle detected in tree - unable to render.
What it means
When rendering a Tree, Spectre.Console walks nodes depth-first, tracking visited nodes in a HashSet<TreeNode> (reference-based). If a TreeNode is reached a second time (HashSet.Add returns false), it throws CircularTreeException — the structure is a graph with a cycle, not a tree, and cannot be laid out. This protects against infinite rendering loops.
Source
Thrown at src/Spectre.Console/Widgets/Tree.cs:88
while (stack.Count > 0)
{
var stackNode = stack.Pop();
if (stackNode.Count == 0)
{
levels.RemoveLast();
if (levels.Count > 0)
{
levels.AddOrReplaceLast(GetGuide(options, TreeGuidePart.Fork));
}
continue;
}
var isLastChild = stackNode.Count == 1;
var current = stackNode.Dequeue();
if (!visitedNodes.Add(current))
{
throw new CircularTreeException("Cycle detected in tree - unable to render.");
}
stack.Push(stackNode);
if (isLastChild)
{
levels.AddOrReplaceLast(GetGuide(options, TreeGuidePart.End));
}
var prefix = levels.Skip(1).ToList();
var renderableLines = Segment.SplitLines(current.Renderable.Render(options, maxWidth - Segment.CellCount(prefix)));
foreach (var (_, isFirstLine, _, line) in renderableLines.Enumerate())
{
if (prefix.Count > 0)
{
result.AddRange(prefix.ToList());
}View on GitHub (pinned to 0acc92fada)
Solutions
- Run a cycle check (DFS with a visited set of TreeNode references) over the root before rendering.
- Ensure each TreeNode instance is added as a child of at most one parent (trees, not graphs).
- If the data is a graph, break cycles by choosing a spanning tree and omitting back-edges.
- Catch CircularTreeException at the render call to fail gracefully with a diagnostic.
Example fix
// before root.AddNode(child); child.AddNode(root); // root now in its own subtree -> cycle AnsiConsole.Write(tree); // after root.AddNode(child); // do not add root (or any ancestor) under its descendants AnsiConsole.Write(tree);
Defensive patterns
Strategy: try-catch
Validate before calling
bool HasCycle(TreeNode root)
{
var seen = new HashSet<TreeNode>();
bool Dfs(TreeNode n)
{
if (!seen.Add(n)) return false;
foreach (var child in n.Nodes)
if (!Dfs(child)) return true; // revisit -> cycle
seen.Remove(n);
return false;
}
return Dfs(root);
} Try / catch
try
{
AnsiConsole.Write(tree);
}
catch (CircularTreeException ex)
{
AnsiConsole.WriteLine($"Refusing to render cyclic tree: {ex.Message}");
} Prevention
- Run a DFS cycle check over TreeNode references before rendering.
- Treat TreeNode as single-parent: each instance belongs to at most one parent.
- When modeling graphs, extract a spanning tree and omit back-edges before rendering.
- Never add an ancestor node into its own descendant subtree.
When it happens
Trigger: Adding a node back into its own subtree: parent.Nodes contains a descendant that (transitively) lists parent again, e.g. node.AddNode(ancestorNode); or reusing the same TreeNode reference such that it is reachable through two paths forming a loop.
Common situations: Building a Tree from graph/dependency data that contains cycles (package dependency cycles, org-chart loops); accidentally adding a parent as a child of one of its descendants; sharing a TreeNode instance across branches in a way that creates a reference loop.
Related errors
AI-assisted analysis of spectreconsole/spectre.console@0acc92fada (2026-08-13).
Data as JSON: /api/errors/3d0854aa3c17804c.
Report an issue: GitHub.