DEV Community

Jack Miras
Jack Miras

Posted on

Laravel's exceptions: part 2 – Custom exceptions

After working with Laravel for a while and learning about its way of handling exception, 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 come in handy, especially when combined with the throw_if and throw_unless functions, which provides 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 an Exception, 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 render 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 where every custom exception can derive from.

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 this kind 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 in a standard way with a basis that can be reusable? For that, we can define an abstraction that all custom exceptions will implement, as the code down 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.

At 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 parameter 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 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 to the context of this exception.

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

Keeping all your messages and texts in Laravel’s translating 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 is done.

Happy coding!

Discussion (3)

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 Author

@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 🙏🏾