Our controllers used to be full of boilerplate try/catch blocks catching vendor-specific SDK exceptions (Stripe, AWS S3, Twilio) and re-throwing custom domain errors or rendering HTTP responses.
// In a global PSR-15 ExceptionHandlingMiddleware:
$this->mapException(StripeExceptionCardException::class, function ($e) {
return new JsonResponse([
'error' => 'payment_failed',
'message' => $e->getMessage()
], 422);
});
// Controllers stay completely clean of transport concerns:
public function charge(Order $order): Response
{
$this->billingService->charge($order);
return new JsonResponse(['status' => 'paid']);
}
Centralizing exception translation in middleware reduced controller verbosity by half and guaranteed that every endpoint formats error responses consistently across the entire API.