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

  1. Wrap your logic in a Closure that receives the subquery builder: ->whereIn('id', function ($q) { $q->select(...); }).
  2. Pass an explicit Query\Builder or Eloquent\Builder — call ->toBase() / ->getQuery() on a relation or scoped builder to get one.
  3. 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

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


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)