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
- Read the message: it names the widget actually open (get_class($widget)) and the class you tried to end — make them match.
- Audit the view from the reported position: every Xxx::begin() must have a matching Xxx::end() in LIFO order, with inner widgets closed first.
- Prefer the one-shot Widget::widget([...]) for widgets that do not need content captured between begin and end.
- 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
- Keep each begin()/end() pair in the same view file and visually adjacent (LIFO order)
- Use Widget::widget([...]) for anything that does not wrap content
- Search views for '::end()' without '::begin()' during code review; many IDEs can pair them
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
- Unexpected {calledClass}::end() call. A matching begin() is
- The "basePath" property must be set.
- Unable to locate view file for view '$view': no active contr
- Unable to resolve view file for view '$view': no active view
- The view file does not exist: $viewFile
AI-assisted analysis of yiisoft/yii2@66f00d18a2 (2026-08-17).
Data as JSON: /api/errors/dbf71b9ad9bedbab.
Report an issue: GitHub.