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);
}
}
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"
}
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());
}
}
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;
}
}
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');
}
}
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)
hi i really love your post, but I need help how do i use the custome exception in my controller. Thanks in advance
@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.
Thanks 🙏🏾
This is great thanks