laravel/framework · error · RuntimeException
Attempted to batch job
Error message
Attempted to batch job [%s], but it does not use the Batchable trait.
What it means
PendingBatch::ensureJobIsBatchable guards Bus::batch() against jobs that cannot participate in a batch. A batched job must use the Illuminate\Bus\Batchable trait so the framework can inject batch metadata (batchId) and the job can query/cancel its own batch. The check is cached per class (static $batchableClasses) for performance.
Solutions
- Add 'use Illuminate\Bus\Batchable;' at the top of the job class body and import the trait.
- If the job is intentionally non-batchable, remove it from the Bus::batch([...]) array and dispatch it separately.
- For closure/invokable batch entries wrapped via dispatch() helpers, ensure the underlying job class uses Batchable.
Example fix
// before
class ProcessPayment implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle(): void { /* ... */ }
}
Bus::batch([new ProcessPayment($invoice)]])->dispatch();
// after
class ProcessPayment implements ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle(): void { /* ... */ }
} Defensive patterns
Strategy: validation
Validate before calling
// Validate all jobs in a batch use Batchable before dispatching
$jobs = [new ProcessPayment($invoice), new SendReceipt($invoice)];
foreach ($jobs as $job) {
if (! isset(class_uses_recursive($job)[\Illuminate\Bus\Batchable::class])) {
throw new \LogicException(get_class($job) . ' must use the Batchable trait to be batched.');
}
}
Bus::batch($jobs)->dispatch(); Type guard
function isBatchableJob(object|string $job): bool
{
return isset(class_uses_recursive(is_object($job) ? $job::class : $job)[\Illuminate\Bus\Batchable::class]);
} Try / catch
try {
Bus::batch($jobs)->dispatch();
} catch (\RuntimeException $e) {
if (str_starts_with($e->getMessage(), 'Attempted to batch job')) {
// log which job class failed the check
logger()->error('Non-batchable job submitted to batch', ['message' => $e->getMessage()]);
}
throw $e;
} Prevention
- Add 'use Batchable;' to any ShouldQueue job template or stub.
- Enforce via arch test (PHPArch/Roave/Arch) that all jobs in app/Jobs passed to Bus::batch use Batchable.
- Audit jobs before refactoring; do not remove Batchable even if IDE marks it 'unused'.
- Document the requirement in team conventions: batchable jobs need the trait.
When it happens
Trigger: Calling Bus::batch([new ProcessPayment($invoice)]) or ->add([...]) on a PendingBatch where ProcessPayment does not 'use Batchable'. Also triggered when chaining ->dispatch() on a PendingBatch that includes a job whose class was stripped of the trait in a refactor.
Common situations: Job was authored before batches existed and never updated. Developer copied a ShouldQueue job template that omits Batchable. Trait removed during cleanup because IDE flagged it as 'unused' (it's used via reflection).
Related errors
- Queue resolver did not return a Queue implementation.
- To enable support for closure jobs, please install the…
- Please implement the prunable method on your model.
- Please implement the prunable method on your model.
- Queueable entity [ ] not found for ID [ ].
AI-assisted analysis of laravel/framework@e0f6eb3518 (2026-08-11).
Data as JSON: /api/errors/cbd7e35c776e267b.
Report an issue: GitHub.
Appendix: 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 e0f6eb3518)