nodejs/node · error · TypeError

Tried to call non recursive loop. Maybe you forgot the 'rec

Error message

Tried to call non recursive loop.  Maybe you forgot the 'recursive' modifier.

What it means

Raised by LoopContext.loop(iterable) when self._recurse is None, i.e. the LoopContext was not created in a recursive loop. The loop() method is the callable injected into templates to recurse; calling it makes sense only inside {% for x in seq recursive %...{% endfor %}. Calling loop(...) in a non-recursive for-loop is almost always a template bug.

Source

Thrown at tools/inspector_protocol/jinja2/runtime.py:404

    @property
    def previtem(self):
        if self._before is _first_iteration:
            return self._undefined('there is no previous item')
        return self._before

    @property
    def nextitem(self):
        if self._after is _last_iteration:
            return self._undefined('there is no next item')
        return self._after

    def __len__(self):
        return self.length

    @internalcode
    def loop(self, iterable):
        if self._recurse is None:
            raise TypeError('Tried to call non recursive loop.  Maybe you '
                            "forgot the 'recursive' modifier.")
        return self._recurse(iterable, self._recurse, self.depth0 + 1)

    # a nifty trick to enhance the error message if someone tried to call
    # the the loop without or with too many arguments.
    __call__ = loop
    del loop

    def __repr__(self):
        return '<%s %r/%r>' % (
            self.__class__.__name__,
            self.index,
            self.length
        )


class LoopContext(LoopContextBase):

View on GitHub (pinned to 1b2de5e052)

Solutions

  1. Add the 'recursive' modifier: {% for node in tree recursive %}...{{ loop(node.children) }}...{% endfor %}.
  2. If recursion is not intended, replace loop(...) with a macro call or a separate {% include %}.
  3. Verify the variable named 'loop' has not been shadowed by a local variable inside the loop body.
  4. Move recursive rendering into a macro that takes the sequence, and call the macro instead of loop().

Example fix

// before
{% for node in tree %}
  {{ node.name }}
  {{ loop(node.children) }}
{% endfor %}
// after
{% for node in tree recursive %}
  {{ node.name }}
  {{ loop(node.children) }}
{% endfor %}
Defensive patterns

Strategy: validation

Validate before calling

# Static validation: scan templates for loop(...) calls and ensure the
# enclosing {% for %} has the 'recursive' modifier.
import re, pathlib
for p in pathlib.Path('templates').rglob('*.html'):
    src = p.read_text()
    for m in re.finditer(r'\{%\s*for\s+([^{]+?)\s+in\s+([^{]+?)( recursive)?\s*%\}(.*?)\{%\s*endfor\s*%\}', src, re.S):
        if 'loop(' in m.group(4) and not m.group(3):
            print(f'{p}: loop() used without recursive modifier')

Type guard

def loop_is_recursive(for_tag_text: str) -> bool:
    return 'recursive' in for_tag_text

Try / catch

# not recoverable at runtime; fix the template
# a syntax/lint check at build time is the right layer

Prevention

When it happens

Trigger: A template uses {{ loop(item.children) }} inside a normal {% for %} block that lacks the 'recursive' modifier. Copy-pasting a recursive macro body into a non-recursive loop. Renaming a variable that shadowed 'loop' so the inner call now hits the outer (non-recursive) LoopContext.

Common situations: Refactoring recursive tree rendering into a flat loop and forgetting to remove loop() calls. Helpers/macros originally written for recursive use and reused in flat contexts. Confusion between loop.index and loop() (calling instead of indexing).

Related errors


AI-assisted analysis of nodejs/node@1b2de5e052 (2026-08-13). Data as JSON: /api/errors/753d589e06ebb6ae. Report an issue: GitHub.