I used to handle errors in Laravel with random try/catch blocks everywhere. It worked… until it didn’t. One day I was building an API endpoint, and I realized every controller was returning a different error format. Some returned message, others returned error, some used 500 for everything (yikes).
- Why create a custom exception in Laravel?
- Step 1: Generate a custom exception class
- Step 2: Define your custom exception (message + code + extra errors)
- Step 3: Throw your custom exception anywhere
- Step 4: Do you still need try/catch?
- Cleaner alternative: handle it globally in Handler.php
- Common mistakes (avoid these)
- Related link
- Final thoughts
That’s when I switched to a cleaner approach: create a custom exception class and let Laravel handle the response consistently. Once I did that, my controllers became simpler and my API responses became predictable.
Why create a custom exception in Laravel?
- Cleaner code: remove repetitive
try/catchblocks from controllers - Consistent API errors: same JSON structure everywhere
- Better debugging: log/report specific failures with context
- Reusable logic: throw the same error from services, jobs, controllers, etc.
Step 1: Generate a custom exception class
Laravel makes this easy with Artisan. Run:
php artisan make:exception CustomExceptionThis will create a file inside app/Exceptions, usually like:
app/Exceptions/CustomException.phpStep 2: Define your custom exception (message + code + extra errors)
Here’s a practical version I use in real projects. It supports:
- a human-readable message
- an HTTP status code (like 400, 401, 403, 404, 422…)
- an optional
errorsarray (great for APIs)
<?php
namespace App\Exceptions;
use Exception;
use Throwable;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
class CustomException extends Exception
{
public array $errors;
public function __construct(
string $message = "Something went wrong.",
int $code = 400,
array $errors = [],
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
$this->errors = $errors;
}
/**
* Return a clean response.
* For APIs this becomes JSON; for web you can customize as needed.
*/
public function render(Request $request): JsonResponse
{
$status = $this->getCode();
if (!is_int($status) || $status < 100) {
$status = 400;
}
return response()->json([
'error' => true,
'code' => $status,
'message' => $this->getMessage(),
'errors' => $this->errors,
], $status);
}
/**
* Optional: log it your way (or skip this and rely on Laravel defaults)
*/
public function report(): void
{
Log::warning($this->getMessage(), [
'code' => $this->getCode(),
'errors' => $this->errors
]);
}
}Tip: Don’t use HTTP 500 for everything. For “bad request” type issues, 400 or 422 is usually more correct.
Step 3: Throw your custom exception anywhere
Now you can throw it from controllers, services, jobs—wherever the error actually happens.
use App\Exceptions\CustomException;
if (!$condition) {
throw new CustomException(
"Invalid request data.",
422,
['field' => ['This field is required.']]
);
}That’s the biggest win: you throw the error once, and Laravel takes care of formatting the response.
Step 4: Do you still need try/catch?
Most of the time, you don’t. If you throw a custom exception and it has a render() method, Laravel will convert it to a proper HTTP response automatically.
But if you want to handle it manually in a specific place, you can still catch it:
use App\Exceptions\CustomException;
try {
// risky logic here
} catch (CustomException $e) {
return response()->json([
'error' => true,
'message' => $e->getMessage(),
], 500);
}Personally, I only do this when I need a very specific fallback behavior. Otherwise, I let the exception handle itself via render().
Cleaner alternative: handle it globally in Handler.php
If you prefer keeping exceptions “dumb” and handling responses in one place, you can register a handler in:
app/Exceptions/Handler.phpExample (inside the register() method):
use App\Exceptions\CustomException;
use Illuminate\Http\Request;
public function register(): void
{
$this->renderable(function (CustomException $e, Request $request) {
return response()->json([
'error' => true,
'code' => $e->getCode() ?: 400,
'message' => $e->getMessage(),
'errors' => $e->errors ?? [],
], $e->getCode() ?: 400);
});
}This is nice if you plan to create multiple custom exceptions and want the same JSON format across all of them.
Common mistakes (avoid these)
- Returning 500 for everything: use 422 for validation-like issues, 404 for missing resources, etc.
- Forgetting to reboot your response format: define one JSON structure and stick to it.
- Throwing exceptions for normal flow: exceptions should be “something went wrong,” not “this is expected behavior.”
- Not logging context: if it helps debugging, include useful details in
report().
Related link
Final thoughts
Once I started using custom exception classes in Laravel, my code got noticeably cleaner. Instead of juggling different error responses in controllers, I now throw meaningful exceptions and let Laravel return a consistent response every time. If you’re building an API, this is one of the fastest ways to make your app feel professional.