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
- Ensure every end() has a matching begin() executed on every code path — move both inside the same conditional block or wrap the whole region.
- Audit partials/layouts for orphan end() calls left behind after a refactor (search for '::end()' without a preceding '::begin()' in the same file chain).
- For stateless widgets, use Widget::widget([...]) which needs no pairing.
- 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
- Put begin() and end() inside the same conditional block — never split the pair across branches
- Never split a widget pair across separately rendered partials
- After test failures mid-render, reset Widget::$stack = [] between test cases to avoid stale/empty-stack surprises
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
- Expecting end() of {widget}, found {calledClass}
- 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/8f7fe0a701c163bf.
Report an issue: GitHub.