A Better Way to Handle Validation Errors

Kevin Smith ·

While working on a project recently, I finally got so frustrated with the awkward way validation errors are typically handled that I was determined to figure out a solution. The usual methods tend to be:

  • Throwing exceptions
  • Returning a bundle of error messages

Both these methods fall really short.

# Exceptions Don't Feel Right

True to their name, exceptions should be reserved almost exclusively for exceptional circumstances that result in program failure. This really isn’t supposed to happen, and now this part of the program can’t continue running. This doesn’t work well for validation errors for a couple of reasons.

First, exceptions give the application a fail-on-first-problem behavior, which means that even though there’s a possibility of multiple validation errors in a method, we can only report back one validation error at a time. That can be pretty frustrating for the user.

Second, it requires the caller to know about the internals of the called class, and that’s not the caller’s responsibility. It’s reasonable to catch a whole category of exceptions (or even all exceptions) when we want to gracefully handle actual program failure, but validation errors each tend to require specific handling which means the caller needs to know exactly which exception to be on the lookout for.

That amounts to a hidden API. Pretty tough to work with, if you ask me.

# Returning a Payload of Errors?

Another common technique is to bundle up all the validation errors and return them in some sort of payload—an array, a custom Errors object, a Domain Payload object, or the Notification pattern.

This improves on the technique of throwing exceptions by allowing a method to report all the validation errors that it might encounter, but it’s clumsy for a couple of other reasons.

It requires the caller to inspect the response and understand what to do with it. That’s not the caller’s responsibility.

The best case scenario, from the caller’s perspective, is that the response is some kind of known collection of error message strings so that the caller doesn’t have to inspect anything. It can just allow the error messages to bubble back up to the surface of the application. But that brings us to the next problem.

To provide fully-formed error messages, the called object has to take on presentational responsibilities. Now rather than merely noting that a particular validation error has occurred, this technique enlists the called object to provide a user-friendly error message as well.

To be clear, the problem here is that the called object isn’t just reporting what happened, it’s deciding how the caller should handle it. Here, just show this message to the user, it says. But what if the caller wants to handle a particular validation error in some other way? It would have to inspect the bundle of errors and perform string matching to find the one it’s looking for. Gross.

# Tell, Don't Ask

Procedural code gets information then makes decisions. Object-oriented code tells objects to do things.

Alec Sharp, “Smalltalk By Example” McGraw-Hill, 1997

Object-oriented design is fundamentally about keeping responsibilities in their proper place. Tell, Don’t Ask is a well-known guideline for designing objects that stick to their responsibilities.

The opposite approach—the procedural approach—is to ask for data and then make decisions based on the result. As we saw above, that approach leaves us with a brittle system with objects that are inextricably bound together. Change even the phrasing of an error message in the called object, and now you’ve broken the behavior of the caller(s).

So how can we leverage Tell, Don’t Ask for validation errors?

The basic idea is this: when a method could have many possible outcomes, it should tell some other object about those significant events.

Create an interface (CreateUserHandler) with a method for each event and require it in the method signature. Think of it as a very localized event handler.

public function createUser(
    CreateUserHandler $handler,
    string $emailAddress,
    string $firstName,
    string $lastName
) : void {
    if (! $this->isNameValid($firstName) || ! $this->isNameValid($lastName)) {
        $handler->nameWasInvalid();
    }
    
    if (! $this->isEmailAddressValid($emailAddress)) {
        $handler->emailAddressWasInvalid();
    }
    
    // If we don't have what we need to create the User, return
    
    // Otherwise, create User and transform to data transfer object $userDto

    $handler->userWasCreated($userDto);
}

When calling the method, pass in any object that implements the interface, leaving it up to that object to handle the called method’s events in a way that makes sense for that part of the system.

For example, if you’re using the Action-Domain-Responder architectural pattern, the responder itself might be the most appropriate event handler.

The action passes in the responder when calling createUser.

class CreateUserAction
{
    /** @var UserService */
    private $service;
    /** @var CreateUserResponder */
    private $responder;
    
    // ...
    
    public function __invoke(Request $request) : Response
    {
        // Get request input data as $attributes

        $this->service->createUser(
            $this->responder,
            $attributes['email'],
            $attributes['firstName'],
            $attributes['lastName']
        );

        return $this->responder->response();
    }
}

The responder uses the events signaled by createUser to generate a response.

class CreateUserResponder implements CreateUserHandler
{
    private $response;
    
    public function emailAddressWasInvalid() : void
    {
        // Add validation error message to $this->response
    }

    public function nameWasInvalid() : void
    {
        // Add validation error message to $this->response
    }

    public function userWasCreated(UserDto $user) : void
    {
        // Create API payload with $user and add it to $this->response
    }

    public function response() : Response
    {
        // Returns HTTP response with application/json content-type
        return $this->response;
    }
}

All the validation errors? Only the first one? Could other method events have an impact on the response?

Sure, maybe. That’s entirely up to the responder to determine how to handle it. That is the responder’s responsibility after all.

This example is for an HTTP request. Hopefully you can imagine how to set up a CLI command for creating a user as well. The CLI command handler class would just pass in its own responder object that implements CreateUserHandler to generate the command-line response, and the createUser method wouldn’t have to change at all.


For what it’s worth, TJ Draper and I developed this technique together as an improvement upon the aforementioned techniques, but I’ll admit that I find it impossible to believe we actually invented this pattern. Seems to be a variant of the delegation pattern, but it doesn’t include that pattern’s requirement of shared context.

If you know the name of the pattern, please let me know.