Sunday, 02. February 2025
Custom Form Validation Error Handling in Laravel (+ HTMX)

I am currently extending this blog with the ability to write comments and like posts - for the two people who read this. For the UI/UX behavior, I am using HTMX, and I have to say, it's a lot of fun. However, I stumbled across some challenges when trying to handle errors during form validation while writing a comment. It took me a while to figure out the proper way to handle this, so I thought it would make a great topic for a blog post.
How Laravel Normally Handles Form Validation Errors
Laravel is a powerful framework that comes with a lot of built-in functionality. Most of the time, this is great since it eliminates the need for boilerplate code. But sometimes, you want to do something different — not the "Laravel way." As a junior developer, this is where things start to feel uncomfortable. But as many people say: You grow from discomfort.
The standard approach to form validation in Laravel involves creating a custom request class
using php artisan make:request RequestName
, defining validation rules in the rules()
method as
an associative array, and providing methods to retrieve values from the input. For example:
public function getMessage(): string {
/** @var string $message */
$message = $this->input('message');
return $message;
}
This is usually enough for 80–90% of cases.
In the controller method, you simply call $request->validated()
, and Laravel takes care of the rest.
But what happens when validation fails? Let's say the message field requires a minimum length of 3 characters,
but the user types hi
, which is one character short. The $request->validated()
method will handle this
by automatically redirecting to the previous page with the validation errors stored in the session.
This default behavior is useful, but what if you want to tweak it slightly - like I did? Laravel
internally throws a ValidationException
when validation fails. This happens in the failedValidation()
method
inside the FormRequest
class, which your custom request extends by default:
protected function failedValidation(Validator $validator)
{
$exception = $validator->getException();
throw (new $exception($validator))
->errorBag($this->errorBag)
->redirectTo($this->getRedirectUrl());
}
Here, Laravel retrieves the exception from the validator, throws it, and redirects the user back to the previous page. While this is great, we want to modify this behavior in our custom request class.
How I Want to Handle Form Validation Errors
For my comment section, I want a different approach: If a user submits a comment with too few characters, Laravel should not respond with a redirect (which would trigger a full-page reload). Instead, the website should behave more like a SPA for this particular feature. The add-comment-form should be replaced with an updated version that includes the validation errors.
To achieve this, I use HTMX, which extends HTML by enabling dynamic content updates via AJAX requests.
This allows me to swap, replace, or modify parts of the page directly from the server without writing JavaScript.
To make this work, I need Laravel to return a specific Blade component - the add-comment-form
- instead of
performing a full-page reload when a validation error occurs.
Configuring Laravel
After several attempts, I arrived at a clean solution. I created a custom request (StoreCommentRequest
)
and overrode the failedValidation()
method to throw a custom exception, StoreCommentException
.
This exception has a dedicated render()
method that returns the add-comment-form Blade template to the client.
HTMX then swaps the content accordingly.
Overriding the failedValidation() Method
Here is the failedValidation()
method I implemented:
protected function failedValidation(Validator $validator)
{
$post = Post::findOrFail($this->getPostID());
throw new StoreCommentException(new ValidationException($validator), $post);
}
First, I retrieve the post from the request, as I need it for the add-comment-form
Blade template.
Then, I throw my custom StoreCommentException
, passing a new ValidationException
along with the
corresponding post. That's all there is to it.
The Custom Exception Class
The StoreCommentException
class is a bit more involved but still fairly straightforward:
class StoreCommentException extends Exception
{
/** @var array<string> */
public array $errors;
public Post $post;
public function __construct(ValidationException $exception, Post $post)
{
parent::__construct($exception->getMessage(), 422);
$this->errors = array_map(fn($error) => is_array($error) && is_string($error[0]) ? $error[0] : '', $exception->errors());
$this->post = $post;
}
public function render(): Response
{
return response()->view(
'components.comments.add-form',
['post' => $this->post, 'errors' => $this->errors],
422
)->header('HX-Retarget', '#add-comment-form');
}
}
In the constructor, I extract error messages from the ValidationException
and store them in the errors property.
The validation errors are stored as an associative array with field names as keys and arrays of error messages
as values:
[
'content' => [
'Error message 1 here', 'Error message 2 here'
]
]
I take the first error message from each field and store it in the errors property.
In the render()
method, I return the Blade template for the add-comment-form
, attaching
all necessary data. Additionally, I add an HX-Retarget header to the response, instructing HTMX to
replace the existing form.
The controller remains unchanged, and I still use $request->validated()
for validation.
Conclusion
By tweaking Laravel’s validation and using HTMX, I got error handling to work more like a modern SPA without full-page reloads. It took some trial and error, but the result is clean and works great!
Customizing the form validation behavior was a bit of a challenge for me. However, after some research and experimentation, I found a working solution. This process reminded me why I love coding: facing an obstacle, feeling unsure about how to overcome it, but eventually figuring it out through persistence. What once seemed difficult becomes easy with effort.
Of course, there's a bit more to making everything work. I skipped the frontend details of HTMX in this post to keep it concise. If you're interested in how to adjust the frontend, let me know (ideally via LinkedIn or the comment section), and I’ll write a Part 2.
Until then, happy coding!
Hallo Test
Thursday, 06. February 2025
- Jan