Writing Custom Exceptions in Laravel 9

Writing Custom Exceptions in Laravel 9

In every codebase, there is always one error that it throws, and will definitely not make sense for it to be shown to the user. An example is the Page not Found or 404 error. In cases like that, it is better to show the user a page explaining that the page they are trying to access does not exist. Another type is a case where you are calling an API and displaying a page depending on the HTTP response from that API. When it comes back with a response that is an error, your code may just throw an error, and the user has no idea what it means. In this article, I will build a Laravel app that makes a request to the OMDB API and handle the exceptions when an error response is received using custom exceptions.

Prerequisites

  • Prior Knowledge of Laravel and PHP

  • Code Editor

  • PHP 8.0+ installed

  • Laravel 9

  • MySQL 8.30 installed

  • Composer installed

  • Node & NPM installed

Setup

To set up your environment, follow the guide on the Laravel page via this link. To create a new Laravel project, type the following commands in your CLI:

composer create-project laravel/laravel custom-exception

This will create a new project in the folder. We can start the project by running the following commands:

cd custom-exception && php artisan serve

The command will run the Laravel development server, and you should see the Laravel welcome page.

Image by Author

Image by Author

We will go ahead and create a resource controller named MoviesController by typing the command below:

php artisan make:controller MoviesController --resource

We will create just two routes for all we are going to do. They are shown below:

<?php
Route::get( '/',[MoviesController::class, 'index']);
Route::get('/results',[ MoviesController::class, 'search_movie']);

The API we will be using is the OMDB API, which is an abstraction of the IMD API. We will write code that calls the API and return a JSON response. For that, we will make a service class called IMDBService. It will be the class that performs the HTTP calls to the API and formats results in JSON to be displayed. The code is below:

namespace App\Services;
use Illuminate\Http\Client;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
Class IMDBService {
    public static function search_title($title)
    {
        $url = config('settings.imdb_url').'/?t='.$title.'&apikey='.config('settings.imdb_key');
        $response = Http::get($url);
        return $response->json();
    }
}

Creating Custom Exception

Because of the way responses are sent by an API, there may be instances where an error response is thrown. We can let the system throw an error, but that will not look good to the user. It is better to show the user what they will understand. For that, we will create a custom exception like below:

namespace App\Exceptions;
use Exception;
class MoviesException extends Exception
{
    public function report()
    {
        return false;
    }
    public function render($request)
    {
        return response()->view('errors.invalid-input', ['message'=> $this->message], 500);
    }
}

This exception class does two things when called, render and report the exception. The render method receives the request object and returns a custom error view that we defined as invalid-input. The report function can be used to send messages about the error through email, slack, or any other service you use. With this class, we can control how we handle the exception and what we show the user. We will then go ahead and create another service class called MoviesService that will call this IMDB class to make requests to the API and handle responses accordingly. In this class, we will return a response to the user when a movie is found and then throw errors when an error is returned. The JSON response comes with a key-value pair we can always check to know if a request is successful. Here is the class:

namespace App\Services;
use App\Services\IMDBService;
use App\Exceptions\MoviesException;
class MoviesService 
{
    public static function get_movie($title)
    {
        $movie = json_decode(json_encode(IMDBService::search_title($title)));
        if ($movie->Response == "True") {
            return $movie;
        } else throw new MoviesException($movie->Error, 1);

    }
}

In the code above, you will notice we are throwing the MoviesException we created. Instead of just passing the response from the server straight to the user, we are sending the error message to the exception class. This will make sure a human-friendly message is shown to the user. The next code to write is the one to show the users the form to search for the movie given the title. We will add the function below to our MoviesController class:

public function search_movie(Request $request)
    {
        $result = MoviesService::get_movie($request->input('title'));
        return $result;
    }

The Full MoviesController Class will look like this:

namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\MoviesService;
class MoviesController extends Controller
{
    /**
    Display the search form.
    @return \Illuminate\Http\Response
    **/
    public function index()
    {
        return view('search');
    }
    public function search_movie(Request $request)
    {
        $result = MoviesService::get_movie($request->input('title'));
        return view('result', ['result' => $result]);
    }
}

The view to display the search page is shown below:

<!-- Search Page -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Search a Movie</title>
    @vite('resources/css/app.css')
</head>
<body>
    <div class="my-60 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
        <!-- We've used 3xl here, but feel free to try other max-widths based on your needs -->
        <div class="mx-auto max-w-2xl">
            <h1 class="mb-5 mx-auto font-bold text-5xl text-center">IMDB Search</h1>
            <form action="/results" method="get">
                @csrf
                <div class="flex">
                <input type="text" name="title" id="title" placeholder="Enter The Movie Title" class="inline-flex w-full rounded-md border-gray-300 pr-12 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" required>
                <button class=" mx-2 inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-6 py-3 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" type="submit">Search</button>
                </div>
            </form>
        </div>
    </div>
</body>
</html>

And the one for the results page is here:

<!-- Results Page -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    @vite('resources/css/app.css')
    <title>Search Result</title>
</head>
    <body>
        <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
            <!-- We've used 3xl here, but feel free to try other max-widths based on your needs -->
            <div class="mx-auto max-w-3xl">
                <div class="mt-8 overflow-hidden bg-white shadow sm:rounded-lg">
                    <div class="px-4 py-5 sm:px-6">
                        <h3 class="text-lg font-medium leading-6 text-gray-900">{{$result->Title}}</h3>
                        <p class="mt-1 max-w-2xl text-sm text-gray-500">IMDB Rating: {{$result->imdbRating}}</p>
                    </div>
                    <div class="border-t border-gray-200 px-4 py-5 sm:p-0">
                        <dl class="sm:divide-y sm:divide-gray-200">
                            <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
                                <dt class="text-sm font-medium text-gray-500">Director</dt>
                                <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">{{$result->Director}}</dd>
                            </div>
                            <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
                                <dt class="text-sm font-medium text-gray-500">Released</dt>
                                <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">{{$result->Released}}</dd>
                            </div>
                            <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
                                <dt class="text-sm font-medium text-gray-500">Genre</dt>
                                <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">{{$result->Genre}}</dd>
                            </div>
                            <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
                                <dt class="text-sm font-medium text-gray-500">Actors</dt>
                                <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">{{$result->Actors}}</dd>
                            </div>
                            <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
                                <dt class="text-sm font-medium text-gray-500">Plot</dt>
                                <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">{{$result->Plot}}</dd>
                            </div>
                        </dl>
                    </div>
                </div>
            </div>
        </div>
    </body>
</html>

For the errors, we will make a view that will render an error depending on what the error message is. The view is shown below:

<div class="my-48 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
  <div class="mx-auto max-w-3xl text-center">
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mx-auto my-5 w-14 h-14 stroke-red-500">
      <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
    </svg>
    <h1 class="font-bold text-3xl font-mono text-red-500">API Error</h1>
  <p class="font-semibold my-4">{{$message}}</p>

  </div>
</div>

The $message variable is the string error message from the API. There are instances where this error message may not make sense to the user, and for that, we can implement a controller that shows different messages that the user can understand instead of what is returned from the API as a response to our request.

Running the Code

Putting everything together to run the code, we will see the search page like this

Image by Author

When you type to search for a movie that exists, this is the response you will get

Image by Author

And if it fails, this is the page you will get

Image by Author

The error message “Movie Not Found!” is the error message from the API response. We left it there because this is very clear to the user. If it were a cryptic message that the user might not understand, we may handle it by making a class to show us a view and message depending on the error code and message we receive from the API.

Conclusion

Handling errors in any application is crucial. Laravel has made it easy to handle almost everything that will be thrown at you, even if you may not know what it is. In this article, we have seen how it's possible to tailor your error messages to the type of responses you receive from an API. I hope I have been available to share something amazing to help you achieve your goal. The link to the repo on GitHub is here. You can use it as a base for any custom exception you want to handle in your Laravel 9 Project. Happy Coding!

Credits