{"record":{"id":"cbd7e35c776e267b","repo":"laravel/framework","slug":"attempted-to-batch-job-s-but-it-does-not-use-t","errorCode":null,"errorMessage":"Attempted to batch job [%s], but it does not use the Batchable trait.","messagePattern":"Attempted to batch job \\[(.+?)\\], but it does not use the Batchable trait\\.","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"src/Illuminate/Bus/PendingBatch.php","lineNumber":110,"sourceCode":"    /**\n     * Ensure the given job is batchable.\n     *\n     * @param  object|array  $job\n     * @return void\n     *\n     * @throws \\RuntimeException\n     */\n    protected function ensureJobIsBatchable(object|array $job): void\n    {\n        foreach (Arr::wrap($job) as $job) {\n            if ($job instanceof PendingBatch || $job instanceof Closure) {\n                return;\n            }\n\n            if (! (static::$batchableClasses[$job::class] ?? false) && ! isset(class_uses_recursive($job)[Batchable::class])) {\n                static::$batchableClasses[$job::class] = false;\n\n                throw new RuntimeException(sprintf('Attempted to batch job [%s], but it does not use the Batchable trait.', $job::class));\n            }\n\n            static::$batchableClasses[$job::class] = true;\n        }\n    }\n\n    /**\n     * Add a callback to be executed when the batch is stored.\n     *\n     * @param  callable  $callback\n     * @return $this\n     */\n    public function before($callback)\n    {\n        $this->registerCallback('before', $callback);\n\n        return $this;\n    }","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/laravel/framework/blob/e0f6eb3518ac29fbbca8529e97d0df7fc9f24481/src/Illuminate/Bus/PendingBatch.php#L92-L128","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before\nclass ProcessPayment implements ShouldQueue\n{\n    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;\n\n    public function handle(): void { /* ... */ }\n}\n\nBus::batch([new ProcessPayment($invoice)]])->dispatch();\n\n// after\nclass ProcessPayment implements ShouldQueue\n{\n    use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;\n\n    public function handle(): void { /* ... */ }\n}","handlingStrategy":"validation","validationCode":"// Validate all jobs in a batch use Batchable before dispatching\n$jobs = [new ProcessPayment($invoice), new SendReceipt($invoice)];\nforeach ($jobs as $job) {\n    if (! isset(class_uses_recursive($job)[\\Illuminate\\Bus\\Batchable::class])) {\n        throw new \\LogicException(get_class($job) . ' must use the Batchable trait to be batched.');\n    }\n}\nBus::batch($jobs)->dispatch();","typeGuard":"function isBatchableJob(object|string $job): bool\n{\n    return isset(class_uses_recursive(is_object($job) ? $job::class : $job)[\\Illuminate\\Bus\\Batchable::class]);\n}","tryCatchPattern":"try {\n    Bus::batch($jobs)->dispatch();\n} catch (\\RuntimeException $e) {\n    if (str_starts_with($e->getMessage(), 'Attempted to batch job')) {\n        // log which job class failed the check\n        logger()->error('Non-batchable job submitted to batch', ['message' => $e->getMessage()]);\n    }\n    throw $e;\n}","preventionTips":["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."],"tags":["bus","batching","queue","trait"],"backgroundTag":null,"analyzedSha":"e0f6eb3518ac29fbbca8529e97d0df7fc9f24481","analyzedAt":"2026-08-11T20:52:37.562Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}