Better way of handling exceptions in Laravel: Trammel — Part 1
Every application needs to handle the runtime errors caused by exceptions. If it’s not done properly, it can become quite disorganized and difficult to reason about. It’s very important to have clean exception handling structure in your project where proper exception handlers are defined and easy to add more without breaking the whole exception handling logic or without creating some bug on the process of adding new if statements.
For that purpose we are going to seek a way to have distinct loosely coupled exception handlers where their responsibility over which exception type they are going to handle are precise.
Typical exception handling in Laravel
In Laravel, as you are probably very familiar, we have one centric place for exceptions and all related logic, that is app/Exception/Handler.php
. At the beginning of a project, Laravel handles quite well most of the exceptions out of the box for you. But over time, as the project is growing, you start to add more and more custom exception logic to that file and after some time it becomes quite big and unmanageable with if else statements are tangled together all over the place.
Then how can we tackle this problem? We will try to follow one of the main principles of software development, that is SRP — Single Responsibility Pattern.
SRP states that a class or a module should do one thing and one thing only. In another word, a class should have only one reason to change and that reason should revolve around what the class actually tries to achieve. So, what does SRP has to do with exception handling in Laravel? Well, we will try to split exceptions to different kinds so that each exception has clear definition and handling of those exceptions are kept in distinct places, e.g. classes.
For that purpose we will use this library I wrote called Trammel which makes our life easier . Most of the details in this article can be found on the documentation of the lib.
Different types of exception
The main reason the Handler class becomes so messy is because trying to handle different kinds of exceptions in the same class. That way the Handler class gets bloated with new helper functions and if else statement and custom formattings. If you are convinced that you have good, intact exception handling mechanism in place in your app and new additions and tweaks hardly necessary, then it’s probably fine and you might get by with that. But this is rarely the case and sooner or later you end up changing the way you format the error messages, adding new properties to the output, etc.
So before digging deeper how we can improve that process, we should first look at the most common types of exception in Laravel. The types of exceptions we will go through of course by any mean is limited what we show here, but it’s likely that they will cause you to handle exceptions differently in each scenario when you encounter one.
We will categorize the exceptions under two main distinct type and each type can be categorized with another main two different exception types. First two is the regular Runtime exceptions and Validation exceptions. We will examine those exceptions under again two main request type, Ajax requests and Json Requests.
So before we dig deeper, we should clarify what is an Ajax and Json requests exactly ?
Ajax Request
As you probably very familiar with Ajax, this is the request type that is issued from the browser via Javascript without page reload. It’s widely used with modern SPA applications leveraging mostly sessions for authentication in the app. When you issue an Ajax request, say with JQuery, like $.ajax
the header X-Requested-With
added to the HTTP request with the value of XMLHttpRequest
allowing you to determine the request in the backend.
In Laravel we useRequest
object to determine wheter the request made via Ajax or not, with the help of it’s $request->ajax()
method. Here is the actual implementation of the method in Laravel.
public function ajax()
{
return $this->isXmlHttpRequest();
}...public function isXmlHttpRequest(): bool
{
return 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
}
JSON Request
JSON is the most common data exchange format used in REST APIs and it is language-independent. What makes a request a json request is basically one special header, that is Content-Type
. The value of that header varies widely as there are lots of different content types available. Mainly the way Laravel determines is it checks wheter the value of Content-Type
contains string +json
or /json
. You might ask yourself why not just check if it is application/json
or not ? Well, again, there are wide variety of json media type probably over 100 and each indicates a json request in itself. Json request term is broad term that encompasses all different media types that uses JSON as messaging format.
Here is how Laravel determines it:
public function isJson()
{
return Str::contains($this->header('CONTENT_TYPE') ?? '', ['/json', '+json']);
}
Exception types in Trammel
Now that we categorized the most common scenarios where we might switch the way we handle exceptions, let’s see the actual implementations. The library Trammel gives us 5 default predefined exception types to reason about. Those are
- Runtime exceptions that occur under Ajax Requests:
AjaxHandler
- Runtime exceptions that occur under JSON Requests:
JsonHandler
- Validation exceptions that occur under JSON Requests:
AjaxValidationHandler
- Validation exceptions that occur under JSON Requests:
JsonValidationHandler
- Regular validation exceptions:
ValidationHandler
Extending the BaseHandler
We will first create Handlers
folder under app/Exceptions
to keep our custom exception handlers. After that we will remove the render
method of app/Exceptions/Handler.php
.This is because we will extend that class with the BaseHandler
class from Trammel. This won’t change any other logic you would implement in Handler
class like reporting exceptions, etc. as BaseHandler
also extends that Laravel’s main Handler
class.So the app/exceptions/Handler.php
class should look like this:
<?php
namespace App\Exceptions;
use Oguz\Trammel\Exception\BaseHandler;
class Handler extends BaseHandler
{
// Remove the render method in the class
}
Behind the scenes BaseHandler
will render our custom exceptions and will provide default output formatting the ones we don’t as well as passing the handling logic to Laravel if it doesn’t have a default handler for that exception. So if you don’t like the default implementations of the handlers of Trammel, go checkout the documentations and customize them the way we show here. Any exception that is not caught by Trammel or your custom handlers will be processed by Laravel. Now let’s move on to defining custom exception handlers and also extending default Trammel exceptions.
Customizing Trammel exceptions
As we pointed out earlier that Trammel provides 5 default exception handlers out of the box. We will show here how we can customize one or two of them and rest would be no different. Let’s take AjaxHandler
for example and customize that. Previously we created app/Exceptions/Handlers
folder, now we will go ahead and add our first handler there. Let’s create a new handler CustomAjaxHandler.php
. This class will have one method which is called handle
and all the exception handling logic will sit inside there. The class should look like following:
<?php
declare(strict_types=1);
namespace App\Exceptions\Handlers;
use Symfony\Component\HttpFoundation\Response;
use Oguz\Trammel\Handlers\AjaxHandler;
use Illuminate\Http\Request;
use Throwable;
class CustomAjaxHandler extends AjaxHandler
{
protected function handle(Request $request, Throwable $exception): Response
{
return response()->json([
'success' => false,
'message' => 'Custom Ajax handler'
]);
}
}`
So let’s go through again about what’s exactly going on here. Imagine you issued an Ajax request from the frontend via JQuery to your backend and you got say ModelNotFoundException
. Now in that case this handle
method will be called along with the underlying Request object and the exception. Here in that scenario the exception Throwable $exception
would be instance of ModelNotFoundException
. After we caught the exception, we return a json response with success
and message
properties. Those are all custom properties and you can modify-add-remove anything you want there. The only requirement here is that you should return a response which of course doesn’t have to be a json response.
Now let’s customize another default handler. Let’s create another file CustomAjaxValidationHandler.php
:
<?php
declare(strict_types=1);
namespace App\Exceptions\Handlers;
use Symfony\Component\HttpFoundation\Response;
use Oguz\Trammel\Handlers\AjaxValidationHandler;
use Illuminate\Http\Request;
use Throwable;
class CustomAjaxValidationHandler extends AjaxValidationHandler
{
protected function handle(Request $request, Throwable $exception): Response
{
return response()->json([
'success' => false,
'message' => 'Custom Ajax validation handler'
]);
}
}
With that last handler the folder structure would look like this:
> app
> Exceptions
> Handlers
CustomAjaxHandler.php
CustomAjaxValidationHandler.php
Handler.php
At this point everything looks good, we defined our handlers for ajax runtime and ajax validation exceptions, extended our Handler with Trammel’s BaseHandler, and this leaves us with the last missing piece registering those handlers into the main Handler file. Let’s open up the app/Exceptions/Handler.php
file and register our custom handlers. To do this we will add a protected
property to Handler class called $handlers
and that will be the last part. After listing our handlers in the $handlers property, that’s all. The handlers will be in place and would start to listen exceptions and would work. Here is how it would look like:
<?php
namespace App\Exceptions;
use App\Exceptions\Handlers\CustomAjaxValidationHandler;
use App\Exceptions\Handlers\CustomAjaxHandler;
use Oguz\Trammel\Exception\BaseHandler;
class Handler extends BaseHandler
{
protected array $handlers = [
CustomAjaxHandler::class,
CustomAjaxValidationHandler::class
];// ...
// Remember no `render` function inside the class
// ...}
And that’s it. Except one small thing. Which is..
Order matters
After adding handlers to Handler class, there is no further step you should take in order to customize exception handlers. But there is one thing you should be aware of is that the order you put handlers matter. The reason being is that under the hood each handler has isResponsible
method determining whether they should process or not the underlying exception. And that method will be called sequentially one by one for each handlers. Now the more specialized the condition, the earlier that handler should be registered in order things to work properly. What do I mean by that? Let’s take a look of the following two examples:
protected function isResponsible(Request $request, Throwable $exception): bool
{
return $request->ajax() &&
$exception instanceof ValidationException;
}
and
protected function isResponsible(Request $request, Throwable $exception): bool
{
return $exception instanceof ValidationException;
}
As you can see both function would return true in case of a ValidationException
. So the first function should run before the second one otherwise the second one would never be run since the function would return true at the second function hence returning that handler.
The idea behind this is very similar to defining Routes. You generally put more generic paths to last. Same idea applies here.
Conclusion
Great. We’ve come so far. We created separate exception handlers and put the logic inside those classes hence separating the concerns. This way we decoupled different exception handling logic to their own classes. It also made it easy to add new handlers. This what we did was applying Chain of Responsibility pattern to exception handling and complying to SRP making our code base more clean and structured.
In this part we explained how to use Trammel and extend it’s default handlers. But down the road we will most likely want to define our custom exception handlers for different types of exception thrown. In the second part of the post, I will explain how you can create custom exception handlers as well as some other features of Trammel.
See you at next part :)