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

  1. Run a cycle check (DFS with a visited set of TreeNode references) over the root before rendering.
  2. Ensure each TreeNode instance is added as a child of at most one parent (trees, not graphs).
  3. If the data is a graph, break cycles by choosing a spanning tree and omitting back-edges.
  4. 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

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.