laravel/framework · error · InvalidArgumentException
A subquery must be a query builder instance, a Closure, or…
Error message
A subquery must be a query builder instance, a Closure, or a string.
What it means
Many query builder methods accept a subquery (whereIn, whereNotIn, whereExists, joinSub, fromSub, etc.). parseSub() normalizes that argument and accepts only three shapes: a Query\Builder, Eloquent\Builder, or Relation; a raw SQL string; or — at the calling sites — a Closure that resolves to a builder. Anything else fails fast with InvalidArgumentException so an invalid subquery never reaches the grammar layer.
Solutions
- Wrap your logic in a Closure that receives the subquery builder: ->whereIn('id', function ($q) { $q->select(...); }).
- Pass an explicit Query\Builder or Eloquent\Builder — call ->toBase() / ->getQuery() on a relation or scoped builder to get one.
- If the subquery is static SQL, pass it as a raw string (DB::raw() not required here; a plain string is accepted).
Example fix
// before
->whereIn('user_id', User::where('active', true))
// after
->whereIn('user_id', User::where('active', true)->pluck('id'))
// or a real subquery:
->whereIn('user_id', function ($q) {
$q->select('id')->from('users')->where('active', true);
}) Defensive patterns
Strategy: validation
Validate before calling
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Relations\Relation;
function isSubqueryable(mixed $q): bool
{
return $q instanceof QueryBuilder
|| $q instanceof EloquentBuilder
|| $q instanceof Relation
|| is_string($q)
|| $q instanceof Closure;
}
if (! isSubqueryable($arg)) {
throw new InvalidArgumentException('Argument must be a builder, Relation, Closure, or SQL string.');
} Type guard
function isSubqueryable(mixed $q): bool
{
return $q instanceof \Illuminate\Database\Query\Builder
|| $q instanceof \Illuminate\Database\Eloquent\Builder
|| $q instanceof \Illuminate\Database\Eloquent\Relations\Relation
|| is_string($q)
|| $q instanceof \Closure;
} Prevention
- Prefer Closure-based subqueries — they read clearly and the builder is constructed for you.
- Call ->toBase() / ->getQuery() on relations and scopes when passing them as subqueries.
- Never pass a Collection or Model instance where a subquery is expected.
When it happens
Trigger: Passing an array, Collection, Model instance, integer, or null where a subquery is expected. Examples: whereIn('id', $model) where $model is a Model (not a builder); joinSub([1,2], 'sub', ...) with an array; whereExists($scalar).
Common situations: Forgetting to call ->getQuery() or ->toBase() on an Eloquent scope/relation; passing a Collection instead of a query builder; passing a relation proxy where a subselect is needed; mis-chaining so a scalar ends up in the subquery slot.
Related errors
- Nested arrays may not be passed to whereIn method.
- Non-numeric value passed as decrement amount for column…
- Non-numeric value passed as increment amount for column…
- Non-numeric value passed to decrement method.
- Non-numeric value passed to increment method.
AI-assisted analysis of laravel/framework@e0f6eb3518 (2026-08-11).
Data as JSON: /api/errors/fba3fddb09f1eccf.
Report an issue: GitHub.
Appendix: source
Thrown at src/Illuminate/Database/Query/Builder.php:436
/**
* Parse the subquery into SQL and bindings.
*
* @param mixed $query
* @return array
*
* @throws \InvalidArgumentException
*/
protected function parseSub($query)
{
if ($query instanceof self || $query instanceof EloquentBuilder || $query instanceof Relation) {
$query = $this->prependDatabaseNameIfCrossDatabaseQuery($query);
return [$query->toSql(), $query->getBindings()];
} elseif (is_string($query)) {
return [$query, []];
} else {
throw new InvalidArgumentException(
'A subquery must be a query builder instance, a Closure, or a string.'
);
}
}
/**
* Prepend the database name if the given query is on another database.
*
* @param mixed $query
* @return mixed
*/
protected function prependDatabaseNameIfCrossDatabaseQuery($query)
{
if ($query->getConnection()->getDatabaseName() !==
$this->getConnection()->getDatabaseName()) {
$databaseName = $query->getConnection()->getDatabaseName();
if (! str_starts_with($query->from, $databaseName) && ! str_contains($query->from, '.')) {View on GitHub (pinned to e0f6eb3518)