DEV Community

Jack Miras
Jack Miras

Posted on • Edited on

Laravel's exceptions: Part 2 – Custom exceptions

After working with Laravel for a while and learning about its way of handling exceptions, I found myself creating my own exceptions, either to simplify some if/else statements or to terminate function calls or even features.

These custom exceptions are helpful, especially when combined with the throw_if and throw_unless functions, which provide a cleaner way of throwing conditional exceptions.

Content

Understanding custom exceptions

Laravel can handle custom exceptions automatically when the exception is created in a certain way. First, the exception has to extend the Exception class, and then, in case you want to render something to the end user, you have to override the render() function.

If you need to create an exception that will extend a class that isn’t the Exception class, Laravel won’t be able to automatically handle the exception, and as a result, you will have to use a try/catch block.

Check the following code example to better understand how a custom exception can be defined.

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\Response;

abstract class MyCustomException extends Exception
{

    public function render(Request $request): Response
    {
        $status = 400;
        $error = "Something is wrong";
        $help = "Contact the sales team to verify";

        return response(["error" => $error, "help" => $help], $status);
    }
}

Enter fullscreen mode Exit fullscreen mode

The result, from the exception above, would be a response with HTTP status 400 and a JSON in the following format:

{
    "error": "Something is wrong",
    "help": "Contact the sales team to verify"
}
Enter fullscreen mode Exit fullscreen mode

To test this example and validate the rendered JSON, you can use plain PHP to throw the exception just like this throw new MyCustomException();

Structuring custom exceptions

Even though the creation of custom exceptions is pretty straightforward, it’s possible to design a more robust architecture around exceptions. The design that will be presented in this post has two premises.

 1) Making exceptions simple and extensible.
 2) Standardize the way error responses are shown.

For them to be simpler and more extensible, we need to define a structure from which every custom exception can derive.

As for standardizing them, it’s required to define how the error will be shown to the end user, and it is important to display your errors in a single, structured manner. Otherwise, front-end applications or other APIs integrating with yours won’t be able to trust your error responses, and these kinds of applications are just horrible to work with.

Making exceptions simple and extensible

So, how can we implement custom exceptions in a way that we can enforce that every exception will comply with the appropriate status code, error, message, and other properties consistently on a basis that can be reused? For that, we can define an abstraction that all custom exceptions will implement, as the code below shows.

<?php

namespace App\Exceptions;

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

abstract class ApplicationException extends Exception
{
    abstract public function status(): int;

    abstract public function help(): string;

    abstract public function error(): string;

    public function render(Request $request): Response
    {
        $error = new Error($this->help(), $this->error());
        return response($error->toArray(), $this->status());
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, an abstract class gets defined to enforce that every exception will implement a status(), help() and error() functions.

In the render() function that we are overriding from the Exception class, we are using the result of the implementation of our help and error methods to create an error object.

The error object is responsible for standardizing the errors in the application; it doesn’t matter whether the error was from an exception or a business rule validation; if it is an error, it will be encapsulated into the error object.

Standardizing errors

To standardize errors, we will create the already mentioned and previously used error object. This object has two main characteristics.

1) Define the data properties that will be shown when rendering the error.
2) Be able to be automatically transformed into JSON by Laravel.

<?php

namespace App\Exceptions;

use JsonSerializable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Contracts\Support\Arrayable;

class Error implements Arrayable, Jsonable, JsonSerializable
{
    public function __construct(private string $help = '', private string $error = '')
    {
    }

    public function toArray(): array
    {
        return [
            'error' => $this->error,
            'help' => $this->help,
        ];
    }

    public function jsonSerialize(): array
    {
        return $this->toArray();
    }

    public function toJson($options = 0.0)
    {
        $jsonEncoded = json_encode($this->jsonSerialize(), $options);
        throw_unless($jsonEncoded, JsonEncodeException::class);
        return $jsonEncoded;
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice that the class declaration implements three interfaces; these interfaces are responsible for making the class capable of being converted into an array or a JSON.

Following the class declaration, we have the constructor where property promotion is getting used to keep the code cleaner; as parameters, we have $help which shows a possible solution to the error getting shown, and $error where the error that happened gets specified.

Next, we have the toArray() function, where the error object gets returned as an array.

Then we have the jsonSerialize() function, where the error object also returns as an array. Here we are applying DRY and just calling the toArray() inside the jsonSerialize() function; since they return the same result, there is no reason for us to repeat ourselves.

Finally, we have the toJson() function, where our object gets converted into a JSON string. Notice that we start by calling the json_encode(). To this function, we are passing as parameters the result from jsonSerializer() previously implemented and the $options from the function itself.

As a result, we should have the $jsonEncoded variable a JSON string matching the information present in our error object; however, in cases where json_encode() can’t transform the array passed as a parameter into JSON, it will return a boolean false.

For that reason, in the next line we’re using the throw_unless() helper to throw an exception in case $jsonEncoded is evaluated to false, and otherwise we return the JSON string in the subsequent line.
Now we just need to define the JsonEncodeException that we conditionally throw in the toJson() function.

Creating the JsonEncodeException

Considering that this exception is a custom exception written specially to handle the JSON conversion of our error object, it makes perfect sense to use our ApplicationException from the previous session.

<?php

namespace App\Exceptions;

use Illuminate\Http\Response;

class JsonEncodeException extends ApplicationException
{
    public function status(): int
    {
        return Response::HTTP_BAD_REQUEST;
    }

    public function help(): string
    {
        return trans('exception.json_not_encoded.help');
    }

    public function error(): string
    {
        return trans('exception.json_not_encoded.error');
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice that once we extend the ApplicationException class, we have the obligation to implement the status, help, and error functions. Inside these functions, we implemented the return of the values that make sense in the context of this exception.

In this specific example, we are returning HTTP status 400, and we are getting translated texts from Laravel’s translating scheme.

Keeping all your messages and texts in Laravel’s translation structure is always a good decision. Having to translate an app after a lot of written code is often more difficult and requires much more refactoring than starting the app with this pattern.


Now our custom exception structure and encapsulation of errors are done.

Happy coding!

Top comments (4)

Collapse
 
rootdefault profile image
rootDefault

hi i really love your post, but I need help how do i use the custome exception in my controller. Thanks in advance

Collapse
 
jackmiras profile image
Jack Miras

@rootdefault if you have implemented the custom exception as explained in this series of articles, you just need to throw the exception at your controller and Laravel will automatically handle the exception for you.

Collapse
 
rootdefault profile image
rootDefault

Thanks 🙏🏾

Collapse
 
hilalnajem3 profile image
hilal-najem3

This is great thanks