In queue-heavy Laravel applications, error handling can get tricky. Until recently, the ThrottlesExceptions
middleware gave you one major lever: the deleteWhen()
method. While useful, it offered only a binary choice—either let jobs retry until exhausted, or delete them outright.
Now, with the new failWhen()
method, Laravel developers can finely tune how exceptions impact job flow, especially when using job chains.
Where deleteWhen()
Falls Short
The deleteWhen()
method is great for situations where you simply want to discard jobs when a particular exception occurs. For instance, if a customer record is missing, retrying the job would be pointless:
public function middleware(): array
{
return [
(new ThrottlesExceptions(2, 10 * 60))
->deleteWhen(CustomerNotFoundException::class)
];
}
The downside? These jobs disappear completely—no failure record, no audit trail, and no chance to trigger custom recovery actions. Worse still, in a chained job sequence, the rest of the chain continues running as if nothing happened.
Enter failWhen()
The new failWhen()
method changes the game. Instead of discarding a job, you can explicitly fail it, ensuring that:
The job’s failure is recorded in the failed jobs table.
Any defined
failed()
handler runs.The entire job chain halts, protecting downstream tasks from running on bad data.
Example:
public function middleware(): array
{
return [
(new ThrottlesExceptions(2, 10 * 60))
->deleteWhen(CustomerNotFoundException::class)
->failWhen(fn (\Throwable $e) => $e instanceof SystemCriticalException)
];
}
Practical Example: Inventory Updates
Imagine an e-commerce system where product stock levels are updated through a series of chained jobs:
Update product quantity
Recalculate metrics
Sync with search index
Notify subscribers
Here’s how you might implement failWhen()
in such a workflow:
public function middleware(): array
{
return [
(new ThrottlesExceptions(3, 5 * 60))
->deleteWhen(ProductDiscontinuedException::class)
->failWhen(function (\Throwable $e) {
return $e instanceof DatabaseConnectionException ||
$e instanceof InventoryLockedException ||
($e instanceof TemporaryNetworkException && $e->isPermanent());
})
->when(fn (\Throwable $e) => $e instanceof TemporaryNetworkException)
->report(function (\Throwable $e) {
return $e instanceof DatabaseConnectionException ||
$e instanceof InventoryLockedException;
}),
];
}
deleteWhen()
: drops jobs for discontinued products (no need to process further).failWhen()
: fails jobs on critical errors (DB outage, locked inventory, or permanent network failure) — halting the chain.when()
: throttles retry attempts for temporary network hiccups.report()
: ensures certain exceptions are logged or reported to monitoring tools.
The failed()
method on the job can then take over for recovery:
public function failed(\Throwable $exception): void
{
Log::error('Inventory update failed', [
'product_id' => $this->product->id,
'exception' => $exception->getMessage(),
]);
$this->product->update([
'update_status' => 'failed',
'last_error' => $exception->getMessage(),
]);
NotifyInventoryTeam::dispatch($this->product, $exception);
}
Key Difference: deleteWhen()
vs failWhen()
deleteWhen()
→ Removes the job permanently, lets chained jobs continue.failWhen()
→ Marks the job as failed, stops the chain, and keeps failure data for debugging.
Why This Matters
The addition of failWhen()
provides a much-needed middle ground between deleting jobs and endlessly retrying them. With it, you get:
Stronger audit trails → failed jobs are preserved in the DB.
Better chain integrity → critical errors stop dependent jobs from executing.
Custom recovery hooks → leverage the
failed()
method for alerts, rollbacks, or notifications.Flexible exception handling → pass exception classes or closures for granular control.