twigphp/Twig · error · RuntimeError
The callable passed to the
Error message
The callable passed to the "%s" %s must be a Closure in sandbox mode.
What it means
CoreExtension::checkArrow validates the arrow-function argument passed to `sort`, `filter`, `find`, `map`, `reduce`, `has some`, or `has every`. In sandbox mode every callable must be a PHP \Closure, because arbitrary callables (like 'strlen' or [Object, 'method']) would let sandboxed templates invoke unvetted code. Twig throws this RuntimeError when a non-Closure callable is used under an enabled sandbox; outside the sandbox it merely triggers a deprecation (since Twig 3.15).
Solutions
- Ensure the arrow argument is a PHP \Closure — in templates arrow functions `v => ...` compile to closures; in custom extension code wrap callables: `$arrow = \Closure::fromCallable($callable);`.
- Clear the Twig cache after enabling/changing the sandbox so templates recompile with sandbox-safe arrow expressions.
- If the callable genuinely cannot be a Closure, expose it as a registered Twig function/filter instead of passing it as an argument.
- Check the twig/twig version: on >=3.15 this is a hard sandbox error; restructure code that relied on string callables in sandboxed templates.
Example fix
// before (PHP custom extension)
$twig->addFilter(new TwigFilter('custom', function ($items) {
return twig_array_filter($items, 'my_predicate'); // string callable
}));
// after
$twig->addFilter(new TwigFilter('custom', function ($items) {
return twig_array_filter($items, \Closure::fromCallable('my_predicate'));
})); Defensive patterns
Strategy: validation
Validate before calling
// PHP, before passing an arrow to filter/map/reduce under a sandbox
function ensureClosureForSandbox(callable $arrow): \Closure {
return $arrow instanceof \Closure ? $arrow : \Closure::fromCallable($arrow);
} Type guard
function isSandboxSafeArrow($arrow): bool { return $arrow instanceof \Closure; } Try / catch
try {
$html = $sandboxedTwig->render('user_template.html.twig', $context);
} catch (\Twig\Error\RuntimeError $e) {
if (str_contains($e->getMessage(), 'must be a Closure in sandbox mode')) {
// reject/fix the template or supply a closure-based implementation
} else { throw $e; }
} Prevention
- Never pass string callables or [obj, 'method'] arrays as arrow arguments in sandboxed templates
- Clear the compiled-template cache whenever sandbox settings change
- In custom extensions, wrap callables with \Closure::fromCallable() before handing them to twig_array_* helpers
- Keep sandbox policy tests that render representative arrow-using templates
When it happens
Trigger: Rendering a template with `|filter`, `|map`, `|sort`, `|reduce`, `find`, `has some/every` through a sandboxed Twig\Environment (Extension\SandboxExtension enabled) while the compiled call site passes a non-Closure callable — typically because the template was compiled by a non-sandboxed parser or a custom extension supplies a plain callable/string function name as the arrow argument.
Common situations: Applications embedding user-supplied templates with the sandbox enabled; cached compiled templates reused between sandboxed and non-sandboxed environments; custom extensions passing `callable` parameters (strings like 'trim') instead of closures; upgrading Twig to >=3.15 where this check was tightened.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- Only "include" tags are allowed within a "sandbox" section.
- The sandbox requires a strict security policy, call…
- The environment passed to
- Value for argument "output_strategy" is required for…
- The "output_strategy" argument of the "render_sandboxed"…
AI-assisted analysis of twigphp/Twig@a414c3a491 (2026-09-13).
Data as JSON: /api/errors/8d8c9e6c61ca108f.
Report an issue: GitHub.
Appendix: source
Thrown at src/Extension/CoreExtension.php:2189
if (!$arrow($v, $k)) {
return false;
}
}
return true;
}
/**
* @internal
*/
public static function checkArrow(bool $isSandboxed, $arrow, $thing, $type): void
{
if ($arrow instanceof \Closure) {
return;
}
if ($isSandboxed) {
throw new RuntimeError(\sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type));
}
trigger_deprecation('twig/twig', '3.15', 'Passing a callable that is not a PHP \Closure as an argument to the "%s" %s is deprecated.', $thing, $type);
}
/**
* @internal to be removed in Twig 4
*/
public static function captureOutput(iterable $body): string
{
$level = ob_get_level();
ob_start();
try {
foreach ($body as $data) {
echo $data;
}
} catch (\Throwable $e) {View on GitHub (pinned to a414c3a491)