Stop Using Try-Catch in Laravel Controllers: Here's the Better Way
If you’re sprinkling try-catch
blocks inside your Laravel controllers, it’s time to stop and take a step back. This pattern might feel safe, like you’re protecting your application from breaking. But in reality, it’s often a code smell — and there’s a cleaner, more scalable approach built right into Laravel.
Let’s walk through why try-catch
in controllers is a bad idea, and what you should be doing instead.
Why try-catch
in Controllers is a Code Smell
1. Breaks Separation of Concerns
Controllers should do one thing: receive input and return a response. When you put error handling logic directly into them, you’re mixing responsibilities. It makes controllers bloated and harder to test or maintain.
2. You’re Ignoring Laravel’s Exception System
Laravel already provides a global exception handler at:
app/Exceptions/Handler.php
This class is designed to catch and respond to unhandled exceptions. It’s where you should centralize error responses, logging, and even custom exception mapping.
3. Inconsistent Error Responses
When every controller handles its own exceptions, your API or web app ends up with inconsistent error formats. One endpoint might return { error: "Something went wrong" }
, while another might return a stack trace or HTML. That’s not good, especially in a public API.
4. Hides Real Problems
Using a broad catch (\Exception $e)
block is dangerous — you might be swallowing critical bugs that should be fixed, not hidden.
The Right Way: Use Laravel’s Global Handler
Laravel routes all exceptions through Handler.php
. Here’s how to use it properly.
1. Throw Custom Exceptions
In your service or domain layer, throw specific exceptions when something goes wrong:
throw new InsufficientBalanceException("Your balance is too low.");
2. Catch and Format Them in Handler.php
Open app/Exceptions/Handler.php
and modify the render()
method:
public function render($request, Throwable $exception)
{
if ($exception instanceof InsufficientBalanceException) {
return response()->json([
'error' => $exception->getMessage(),
], 400);
}
return parent::render($request, $exception);
}
Now every time that exception is thrown, it will return a proper response — and your controller stays clean.
Other Considerations
✅ Validation? Use Form Requests
Don’t wrap validation logic in try-catch
. Use Laravel’s FormRequest
:
php artisan make:request StoreUserRequest
This way, validation errors are automatically turned into JSON with status 422.
✅ Auth and Permissions? Use Middleware
Avoid catching permission errors in your controller. Instead, use middleware like auth
, can
, or custom gates. Let Laravel throw AuthorizationException
, and handle it globally.
✅ Want Logs? Use the report()
Method
If you want to log certain exceptions or send them to an external service like Sentry:
public function report(Throwable $exception)
{
if ($exception instanceof DomainException) {
// Send to external service
}
parent::report($exception);
}
Finally
Writing clean Laravel code means trusting the framework. Laravel gives you powerful tools to manage exceptions at a global level. By centralizing your error handling, your code becomes:
- More readable
- More testable
- Easier to maintain
- More consistent across endpoints
So next time you catch yourself typing try { ... } catch (Exception $e)
inside a controller, stop. There’s a better way — and it’s already in your Laravel toolbox.
Comments ()