← Back

Custom Exceptions in Laravel

Many developers catch generic exceptions everywhere, which hides whether an error is minor or critical. Custom exceptions let you cleanly exit bad states and return structured responses without scattered if statements or early returns.

The Problem

Here’s how many developers handle exceptions in controllers:

public function update(UpdateUserRequest $request)
{
    try {
        $user = $this->userService->activate($request->user_id);
        return response()->noContent();
    } catch (Exception $e) {
        return response()->json(['error' => 'Something went wrong'], 500);
    }
}

This catches everything—database errors, validation errors, network failures—and returns the same generic response. Debugging becomes a nightmare.

Custom Exceptions

Create an exception that knows how to render itself:

namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class InvalidUserStatusException extends Exception
{
    public function __construct(string $status)
    {
        parent::__construct("User status '{$status}' is invalid for this operation.");
    }

    public function render(Request $request): Response
    {
        return response()->json([
            'error' => $this->getMessage()
        ], 422);
    }
}

Usage

Your service throws the exception when something is wrong:

class UserService
{
    public function activate(int $userId): User
    {
        $user = User::findOrFail($userId);

        if ($user->status !== 'pending') {
            throw new InvalidUserStatusException($user->status);
        }

        $user->status = 'active';
        $user->save();

        return $user;
    }
}

Your controller stays clean:

public function update(UpdateUserRequest $request)
{
    $this->userService->activate($request->user_id);
    return response()->noContent();
}

No try-catch needed. Laravel catches the exception and calls its render() method automatically.

Why This Works

When you throw a custom exception, it ejects out of the current flow entirely. Laravel’s exception handler catches it and:

  1. Calls the exception’s render() method if it exists
  2. Returns that response to the client
  3. Logs it if you’ve defined a report() method

Other exceptions (database failures, etc.) still propagate normally to your error monitoring tools like Sentry.

This pattern keeps your controllers thin, centralises error responses, and makes debugging straightforward.