twigphp/Twig · error · LogicException

The tag of a node can only be set once.

Error message

The tag of a node can only be set once.

What it means

Node::setNodeTag() is an internal one-shot API: each node can receive its "tag" (the template tag like `if`, `for`, `block`) exactly once. Calling it a second time on an already-tagged node is a LogicException because it indicates a parser/visitor bug or duplicate tagging, not user-input validation. Twig throws this to enforce the invariant that a node's tag is immutable after assignment.

Solutions

  1. Guard the call with $node->hasNodeTag() (or check a local flag) before calling setNodeTag().
  2. Ensure your NodeVisitor only sets the tag once, e.g. set it in enterNode() and track visited nodes in the visitor state.
  3. Do not cache or reuse Node instances between parse/compile runs; create fresh nodes per compilation.
  4. Update the Twig version if this fires during normal core parsing (a known core/extension incompatibility).

Example fix

// before
public function enterNode(\Twig\Node\Node $node, Environment $env): Node
{
    $node->setNodeTag('custom');
    return $node;
}
// after
public function enterNode(\Twig\Node\Node $node, Environment $env): Node
{
    if (!$node->hasNodeTag()) {
        $node->setNodeTag('custom');
    }
    return $node;
}
Defensive patterns

Strategy: validation

Validate before calling

if (!$node->hasNodeTag()) {
    $node->setNodeTag('custom');
}

Try / catch

try {
    $node->setNodeTag($tag);
} catch (\LogicException $e) {
    // tag already set; treat as no-op
}

Prevention

When it happens

Trigger: Calling setNodeTag() twice on the same Node instance, e.g. a custom NodeVisitor that assigns a tag in enterNode() for every traversal pass, or subparse()/the parser re-visiting a node and re-tagging it. Any custom code path that re-enters subparse() with cached Node objects.

Common situations: Writing a custom Twig extension whose node visitor sets tags on nodes without checking hasNodeTag() first; running the parser over the same node twice (double compilation passes); caching Node instances across template compilations and re-running the parser on them.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of twigphp/Twig@a414c3a491 (2026-09-13). Data as JSON: /api/errors/5b0c40f6d825002f. Report an issue: GitHub.

Appendix: source

Thrown at src/Node/Node.php:159

    {
        return $this->documentation;
    }

    /**
     * @internal
     */
    public function setDocumentation(?string $documentation): void
    {
        $this->documentation = $documentation;
    }

    /**
     * @internal
     */
    public function setNodeTag(string $tag): void
    {
        if ($this->tag) {
            throw new \LogicException('The tag of a node can only be set once.');
        }

        $this->tag = $tag;
    }

    public function hasAttribute(string $name): bool
    {
        return \array_key_exists($name, $this->attributes);
    }

    public function getAttribute(string $name)
    {
        if (!\array_key_exists($name, $this->attributes)) {
            throw new \LogicException(\sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class));
        }

        $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true;
        if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) {

View on GitHub (pinned to a414c3a491)