
laravel 自定义 artisan 命令中分发的队列任务若抛出异常,默认不会触发全局异常处理器(handler.php),需通过 job 的 `failed()` 方法捕获并处理失败逻辑,如发送告警邮件。
在 Laravel 中,队列任务(Job)的异常生命周期与普通 HTTP 请求完全不同:当 Job 在队列中执行时抛出未捕获异常,Laravel 不会经过 App\Exceptions\Handler::render() 或 report() 方法,而是直接进入队列失败处理流程。因此,将异常逻辑写在 try-catch 块中或依赖全局异常处理器是无效的。
✅ 正确做法:利用 Laravel 内置的 Job 失败回调机制 —— 实现 failed() 方法:
-
确保已创建 failed_jobs 表(Laravel 8+ 默认需要):
php artisan queue:failed-table php artisan migrate
-
在你的 Job 类中定义 failed() 方法:
namespace App\Jobs;
use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Mail; use Throwable;
class ProcessPayment implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle()
{
// 模拟可能失败的操作(如调用第三方 API)
throw new \Exception('Payment gateway timeout');
}
/**
* 当任务执行失败时调用
*/
public function failed(Throwable $exception)
{
// 记录日志(可选)
\Log::error('Job failed: ' . get_class($this), [
'exception' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// 发送定制化告警邮件(支持按异常类型区分处理)
if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
Mail::to('ops@example.com')->send(new PaymentConnectionFailedMail($exception));
} elseif ($exception instanceof \Exception) {
Mail::to('dev@example.com')->send(new GenericJobFailureMail($exception));
}
}}
? **关键说明**: - `failed()` 方法仅在 Job **超出重试次数后最终失败**时触发(默认重试次数由 `--tries` 参数或 `retryAfter()`/`tries` 属性控制); - 若需立即响应首次失败,可在 `handle()` 中手动调用 `$this->fail($exception)`,但不推荐替代标准重试机制; - `failed()` 接收 `Throwable` 类型参数,可安全进行类型判断与差异化处理; - 该方法运行在队列工作进程上下文中,**无法访问请求相关对象(如 `request()`, `session()`)**,请勿在此处依赖 HTTP 环境。 ? **进阶建议**: - 使用 `php artisan queue:listen --tries=3` 或配置 `config/queue.php` 中的 `redis.options.retry_after` 统一控制重试策略; - 结合 Laravel Horizon 可视化监控失败任务、重试、延迟等状态; - 对敏感业务(如支付、库存扣减),建议在 `failed()` 中触发补偿操作(如回滚订单状态、释放锁)。 通过规范使用 `failed()` 方法,你既能精准捕获各类 Job 异常,又能实现统一、可靠、可扩展的错误通知体系——无需侵入式 try-catch,也无需绕过 Laravel 队列设计哲学。










