laravel/framework · error · RuntimeException

Attempted to batch job [%s], but it does not use the Batchab

Error message

Attempted to batch job [%s], but it does not use the Batchable trait.

What it means

PendingBatch::__construct()/add() calls ensureJobIsBatchable(), which uses class_uses_recursive() to verify each job uses the Illuminate\Bus\Batchable trait. Jobs in a batch must be Batchable so the framework can attach the batch ID and report progress. A missing trait throws this RuntimeException immediately.

Source

Thrown at src/Illuminate/Bus/PendingBatch.php:110

    /**
     * Ensure the given job is batchable.
     *
     * @param  object|array  $job
     * @return void
     *
     * @throws \RuntimeException
     */
    protected function ensureJobIsBatchable(object|array $job): void
    {
        foreach (Arr::wrap($job) as $job) {
            if ($job instanceof PendingBatch || $job instanceof Closure) {
                return;
            }

            if (! (static::$batchableClasses[$job::class] ?? false) && ! isset(class_uses_recursive($job)[Batchable::class])) {
                static::$batchableClasses[$job::class] = false;

                throw new RuntimeException(sprintf('Attempted to batch job [%s], but it does not use the Batchable trait.', $job::class));
            }

            static::$batchableClasses[$job::class] = true;
        }
    }

    /**
     * Add a callback to be executed when the batch is stored.
     *
     * @param  callable  $callback
     * @return $this
     */
    public function before($callback)
    {
        $this->registerCallback('before', $callback);

        return $this;
    }

View on GitHub (pinned to bd6b5437e6)

Solutions

  1. Add 'use \Illuminate\Bus\Batchable;' inside the job class.
  2. Inside the job's handle(), use $this->batch() to access the batch (only available with the trait).
  3. If batching closures or nested PendingBatch instances, note those are exempt; do not pass raw non-Batchable objects.

Example fix

// before
class ProcessPodcast implements ShouldQueue
{
    public function handle() { ... }
}
Bus::batch([new ProcessPodcast()])->dispatch();

// after
use Illuminate\Bus\Batchable;

class ProcessPodcast implements ShouldQueue
{
    use Batchable;

    public function handle()
    {
        $batch = $this->batch();
    }
}
Bus::batch([new ProcessPodcast()])->dispatch();
Defensive patterns

Strategy: type-guard

Validate before calling

foreach ($jobs as $job) {
    if (! in_array(\Illuminate\Bus\Batchable::class, class_uses_recursive($job), true)) {
        throw new \LogicException(get_class($job).' must use the Batchable trait to be batched.');
    }
}
Bus::batch($jobs)->dispatch();

Type guard

function isBatchable(object $job): bool
{
    return isset(class_uses_recursive($job)[\Illuminate\Bus\Batchable::class]);
}

$batchable = array_filter($jobs, 'isBatchable');

Try / catch

try {
    Bus::batch($jobs)->dispatch();
} catch (\RuntimeException $e) {
    if (str_contains($e->getMessage(), 'does not use the Batchable trait')) {
        // add the trait to the offending class and retry
    }
    throw $e;
}

Prevention

When it happens

Trigger: Calling Bus::batch([new MyJob()])->dispatch() where MyJob extends a queueable base but does not 'use Batchable;'. Passing a plain object job into Bus::batch() or PendingBatch::add().

Common situations: Adding an existing queued job to a new batch without retrofitting the trait. Copying a job class that predates job batching. Forgetting the trait after scaffolding with make:job.

Related errors


AI-assisted analysis of laravel/framework@bd6b5437e6 (2026-08-06). Data as JSON: /data/errors/cbd7e35c776e267b.json. Report an issue: GitHub.