Back

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!

Laravel

Comments

Login or Register to write comments and like posts.

Hallo Test

Thursday, 06. February 2025

- Jan


GitHub LinkedIn

© 2025