yiisoft/yii2 · error · InvalidCallException

Expecting end() of {widget}, found {calledClass}

Error message

Expecting end() of {widget}, found {calledClass}

What it means

Thrown by yii\base\Widget::end() when the widget popped off the static stack is not the class on which end() was called. begin() pushes the created widget onto Widget::$stack and end() pops the last one; fluent widget configs can make end() resolve to the resolved class via $_resolvedClasses, so a mismatch means begin()/end() calls are improperly nested or interleaved between different widget classes.

Source

Thrown at framework/base/Widget.php:125

    public static function end()
    {
        if (!empty(self::$stack)) {
            $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();

View on GitHub (pinned to 66f00d18a2)

Solutions

  1. Read the message: it names the widget actually open (get_class($widget)) and the class you tried to end — make them match.
  2. Audit the view from the reported position: every Xxx::begin() must have a matching Xxx::end() in LIFO order, with inner widgets closed first.
  3. Prefer the one-shot Widget::widget([...]) for widgets that do not need content captured between begin and end.
  4. Enable view linting/IDE inspections that pair begin/end calls, and keep widget blocks short.

Example fix

// before
ActiveForm::begin();
Panel::begin();
    echo $content;
ActiveForm::end(); // throws: open widget is Panel, not ActiveForm
Panel::end();

// after
ActiveForm::begin();
Panel::begin();
    echo $content;
Panel::end(); // close inner widget first
ActiveForm::end();
Defensive patterns

Strategy: validation

Validate before calling

// Widget::$stack is public static — inspect it before end()
$stack = \yii\base\Widget::$stack;
if (empty($stack) || !(end($stack) instanceof \yii\widgets\ActiveForm)) {
    // pairing bug: do not call ActiveForm::end() here
    \Yii::warning('ActiveForm::end() skipped: widget stack mismatch', 'widgets');
    return;
}
\yii\widgets\ActiveForm::end();

Try / catch

try {
    Panel::end();
} catch (\yii\base\InvalidCallException $e) {
    // message names both classes; repair stack/flags and rethrow in debug
    \Yii::error('Widget pairing broken: ' . $e->getMessage(), 'widgets');
    throw $e;
}

Prevention

When it happens

Trigger: WidgetA::begin() ... WidgetB::end(); nesting two widgets and closing the outer one first (begin A, begin B, end A, end B); copy-pasting a block and changing only the end() class name; a missing end() for the inner widget so the outer end() pops the wrong instance; calling begin() conditionally but end() unconditionally is the sibling case.

Common situations: Hand-edited view layouts mixing widget wrappers (e.g. yii\bootstrap\ActiveForm vs yii\widgets\ActiveForm end() calls swapped); long view files where an inner widget's end() was accidentally deleted; merging template branches and losing one end(); inheritance where begin() was called on a parent alias but end() on a subclass with a different resolved class.

Related errors


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