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
- Guard the call with $node->hasNodeTag() (or check a local flag) before calling setNodeTag().
- Ensure your NodeVisitor only sets the tag once, e.g. set it in enterNode() and track visited nodes in the visitor state.
- Do not cache or reuse Node instances between parse/compile runs; create fresh nodes per compilation.
- 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
- Always check hasNodeTag() before setNodeTag().
- Set tags in exactly one place (e.g. a single enterNode() branch) in visitors.
- Never reuse Node instances across parse/compile passes.
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
- A block must start with a tag name.
- The lexer or the parser ended up in an unsupported state.
- The "deprecation_info" option must be an instance of
- A block chain cannot contain templates from different Twig…
- Unable to add a node visitor as extensions have already…
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)