twigphp/Twig · error · LogicException

Cannot pop state without a previous state.

Error message

Cannot pop state without a previous state.

What it means

The Lexer maintains a stack of lexer states (pushState/popState) as it tokenizes templates. popState is called when a construct like a closing bracket, string end, or interpolation end is reached; if the state stack is empty there is nothing to pop, meaning the lexer's state machine is out of sync — usually a lexer state was popped more times than pushed, or never pushed.

Solutions

  1. Search your code for calls to pushState/popState on the lexer and ensure every popState has a matching earlier pushState.
  2. If you have a custom Lexer or expression parser changing lexer behavior, verify the state stack starts balanced: push the base state before lexing begins.
  3. Reduce the template that triggers it to a minimal snippet and check for unmatched closing delimiters (}} , %}, ]), strings, or interpolation ends.
  4. If the trigger is in a third-party lexer extension, update it or report the unbalanced state transition to its maintainer; as a last resort catch the LogicException at render/lex time to fail gracefully.

Example fix

// before (custom lexer code)
if ($this->punctuation === ']') { $this->popState(); }
// after — only pop when a state was actually pushed
if ($this->punctuation === ']' && $this->hasPushedState) { $this->hasPushedState = false; $this->popState(); }
Defensive patterns

Strategy: type-guard

Validate before calling

if (0 === count($lexer->getStateStack())) { // expose or track your own mirror of pushed states
    return; // nothing to pop
}

Type guard

function canPopState(array $states): bool { return count($states) > 0; }

Try / catch

try {
    $lexer->popState();
} catch (\LogicException $e) {
    // log unbalanced lexer state; reset lexer state stack and abort tokenizing this template
}

Prevention

When it happens

Trigger: Calling the protected popState path via a custom lexer subclass that pops without a matching pushState; a lexer state function (lexBlock, lexVar, lexString, lexInterpolation) reaching an exit condition that pops state when no state was pushed (e.g. malformed template input like an unmatched closing delimiter the push logic never opened).

Common situations: Custom Twig lexer extensions or custom delimiters whose begin/end logic is asymmetric; corrupted template syntax that drives the lexer into an unexpected transition; a bug in a forked Lexer where the initial state isn't pushed before the first pop.

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/ea1ba44d8e617ac1. Report an issue: GitHub.

Appendix: source

Thrown at src/Lexer.php:656

            // an operator with a space can be any amount of whitespaces
            $r = preg_replace('/\s+/', '\s+', $r);

            $regex[] = $r;
        }

        return '/'.implode('|', $regex).'/A';
    }

    private function pushState($state): void
    {
        $this->states[] = $this->state;
        $this->state = $state;
    }

    private function popState(): void
    {
        if (0 === \count($this->states)) {
            throw new \LogicException('Cannot pop state without a previous state.');
        }

        $this->state = array_pop($this->states);
    }

    private function checkBrackets(string $code): void
    {
        // opening bracket
        if (\in_array($code, $this->openingBrackets, true)) {
            $this->brackets[] = [$code, $this->lineno];
        } elseif (\in_array($code, $this->closingBrackets, true)) {
            // closing bracket
            if (!$this->brackets) {
                throw new SyntaxError(\sprintf('Unexpected "%s".', $code), $this->lineno, $this->source, columnno: $this->source->getColumn($this->cursor));
            }

            [$expect, $lineno] = array_pop($this->brackets);
            if ($code !== str_replace($this->openingBrackets, $this->closingBrackets, $expect)) {

View on GitHub (pinned to a414c3a491)