yiisoft/yii2 · error · InvalidCallException

Unexpected {calledClass}::end() call. A matching begin() is

Error message

Unexpected {calledClass}::end() call. A matching begin() is not found.

What it means

Thrown by yii\base\Widget::end() when the static widget stack Widget::$stack is empty, i.e. end() was called with no preceding begin() still pending. begin() pushes onto the shared stack and end() pops; an empty stack means every begin() was already closed or never ran, so there is nothing to end.

Source

Thrown at framework/base/Widget.php:128

            $widget = array_pop(self::$stack);

            $calledClass = self::$_resolvedClasses[get_called_class()] ?? get_called_class();

            if (get_class($widget) === $calledClass) {
                /** @var static $widget */
                if ($widget->beforeRun()) {
                    $result = $widget->run();
                    $result = $widget->afterRun($result);
                    echo $result;
                }

                return $widget;
            }

            throw new InvalidCallException('Expecting end() of ' . get_class($widget) . ', found ' . get_called_class());
        }

        throw new InvalidCallException('Unexpected ' . get_called_class() . '::end() call. A matching begin() is not found.');
    }

    /**
     * Creates a widget instance and runs it.
     * The widget rendering result is returned by this method.
     * @param array $config name-value pairs that will be used to initialize the object properties
     * @return string the rendering result of the widget.
     * @throws \Throwable
     */
    public static function widget($config = [])
    {
        ob_start();
        ob_implicit_flush(false);
        try {
            $config['class'] = get_called_class();
            /** @var self $widget */
            $widget = Yii::createObject($config);
            $out = '';

View on GitHub (pinned to 66f00d18a2)

Solutions

  1. Ensure every end() has a matching begin() executed on every code path — move both inside the same conditional block or wrap the whole region.
  2. Audit partials/layouts for orphan end() calls left behind after a refactor (search for '::end()' without a preceding '::begin()' in the same file chain).
  3. For stateless widgets, use Widget::widget([...]) which needs no pairing.
  4. In long-running workers/tests, treat leftover or drained Widget::$stack as a signal to reset state between render runs (Widget::$stack = []).

Example fix

// before
<?php if ($showSearch): ?>
    <?= SearchBox::begin() ?>
<?php endif; ?>
... content ...
<?= SearchBox::end() ?> // begin() skipped when $showSearch is false -> throws

// after
<?php if ($showSearch): ?>
    <?= SearchBox::begin() ?>
    ... content ...
    <?= SearchBox::end() ?>
<?php else: ?>
    ... content ...
<?php endif; ?>
Defensive patterns

Strategy: validation

Validate before calling

// Guard before an end() that may be orphaned by control flow
if (empty(\yii\base\Widget::$stack)) {
    \Yii::warning('Skipped orphan Widget::end() call.', 'widgets');
    return; // or restructure so begin()/end() share the same branch
}
SearchBox::end();

Prevention

When it happens

Trigger: Calling Widget::end() standalone in a view; a conditional begin() with an unconditional end(): if ($cond) { Xxx::begin(); } ... Xxx::end(); an earlier exception mid-render unwinding/popping the stack so a later end() finds it empty (e.g. Yii's error handling or Gii/DeepClone style stack resets between tests); begin() in one partial and end() in another that is rendered independently.

Common situations: Editing template conditionals around widget wrappers and forgetting to move the end(); splitting a widget wrapper across layout/partial boundaries during refactoring; test suites rendering partial views out of order after an earlier failure left the stack drained.

Related errors


AI-assisted analysis of yiisoft/yii2@66f00d18a2 (2026-08-17). Data as JSON: /api/errors/8f7fe0a701c163bf. Report an issue: GitHub.