Exceptions and talking back to the user

Posted on by Matthias Noback

Exceptions - for exceptional situations?

From the Domain-Driven Design movement we've learned to go somewhat back to the roots of object-oriented design. Designing domain objects is all about offering meaningful behavior and insights through a carefully designed API. We know now that domain objects with setters for every attribute will allow for the objects to be in an inconsistent state. By removing setters and replacing them with methods which only modify the object's state in valid ways, we can protect an object from violating domain invariants.

This style of modeling domain aggregates (entities and value objects) results in methods that modify state, but only after checking if the change is allowed and makes sense, given the rules dictated by actual domain knowledge. In terms of code, these methods may make some simple assertions about the arguments passed to it: the number of things, the range of a number, the format of a string. When anything is wrong about a provided argument, the domain object simply throws an exception. If everything is okay, the changes will be accepted by modifying any attribute involved.

So exceptions in your (object-oriented) domain model are not merely meant to signal an exceptional situation. They can be used to prevent invalid or unsupported usage of an object. By offering well-named methods (with sensible parameters) for changing the object's state, and by being very precise about throwing exceptions when invalid use is imminent, you make your domain objects usable in only one way: the way that makes sense. This is the exact opposite of how your domain objects end up looking if you generate getters and setters for every attribute.

Using exceptions for validation

You might say that, given that the object protects itself from ending up in an in valid state, it basically validates itself. However, it won't be good for our application's usability if we'd use these domain-level exceptions as messages to the user. The thing is:

  • Exceptions get thrown ad hoc, whenever something threatens the consistency of the domain object. You can only catch one of these exceptions and turn it into an error message for the user. If the user tries to fix the issue by making a change, sending the form again, and they manage to get past this exception, there may be another exception just around the corner. This will be very frustrating for the user, as it will feel like trial-and-error.
public function processDelivery(int $quantity, string $batchNumber): void {
    if ($quantity <= 0) {
        /*
         * If the user triggers this exception, they have to resubmit,
         * which may trigger another exception further down the line...
         */
        throw new InvalidArgument(
            'Quantity should be more than 0.'
        );
    }

    if (empty($batchNumber) {
        throw new InvalidArgument(
            'This delivery requires a batch number.'
        );
    }

    // ...
}
  • Domain exceptions aren't always about validating the data the user provides. They often signal that something is about to happen that can't logically happen, like a state change that isn't allowed or conceptually possible. E.g. once an order has been cancelled, it shouldn't be possible to change its delivery address, because that makes no sense:
public function changeDeliveryAddress(...): void {
    if ($this->wasCancelled) {
        throw new InvalidState('You cannot change ...');
    }

    // ...
}
  • Exception messages may contain more information than you'd like to share with the user.
  • Validation errors often require internationalization (i18n). They need localization in the sense that numbers should be formatted according to the user's locale. Of course, they often need translation too. Exceptions aren't naturally usable for translation, because they contain special values hard-coded into their messages.
throw new InvalidArgument(
    'Product 21 has a stock level of 100, but this delivery has a quantity of 200.'
);

Translation needs a template message which will be translated, after which the variables it contains will be replaced by their real values.

So exceptions thrown to protect domain invariants are not validation messages all by themselves. They are there to prevent bad things from happening to your domain objects. They are not useful if what you want is talk back to the user. If you're looking for a dialogue with the user about what they're trying to achieve, you should be having it in a layer of the application that's closer to the user.

There are several options there:

  • You could make the user interface smarter and conveniently provide some validation errors while the user is still filling in a form, etc.
  • Once the user has submitted some data, you could validate it when you're inside the controller. If necessary, you could then update the view with validation errors.

Improving the UI to prevent backend exceptions

If everything looks good, it should not be necessary to rely on domain-level exceptions to warn the user about something they're doing wrong. The UI and the controller should be able to let the user safely reach their goal. Often, you'll need to make the UI just a little bit smarter, by providing it with some (backend) knowledge.

For example, if the UI has a delete button for deleting an order, pressing it will trigger a number of validation steps in the backend code to figure out whether or not the order can in fact be deleted. If you show a delete button to the user, and they click on it, wait for a few seconds, and only then find out that deleting is actually not allowed, they will be disappointed. You can prevent this disappointment, by doing the checks beforehand, when the page gets loaded. That way, you can tell the frontend whether or not to show the delete button in the first place.

Of course, the backend will still make the checks, but the chance that the user can't do what they want to do, will be quite small.

User-facing exceptions

In the project I'm currently working on, there is a sort of mixed-style exception in use: the so-called App_Exception. When such an exception bubbles up to the point where it would be rendered as a 500 Internal server error page, it will instead be converted to a user-friendly error message shown in the UI. It's often an application service or domain-level exception that's meant to reach the user - unlike all the other types of exceptions, I should add. These exceptions will be logged, but never shown to the user. App_Exception however has a translatable message, aiming to help the user figure out what's wrong.

I liked the underlying idea; sometimes you really want to talk back to the user, telling them that you can't continue with their request. If you can't easily improve the UI to not let the user make the - somehow - bad request in the first place, something like an App_Exception can be very convenient (it may not be the most intention-revealing name of course).

My wish list for improving this thing was:

  • The user-facing exception message itself should be translatable, to be as helpful to the user as possible.
  • The user-facing exception message should have contextual data, to be even more helpful (i.e. "You can't deliver 3 items, since only 2 were ordered.").
  • It should be possible for the exception's developer-facing message (the message that ends up in the logs), to be more detailed or simply just different than the user-facing message.
  • It should be easy to use custom, domain-specific exception type classes, instead of one generic class, like App_Exception.

This reminded me of how the Symfony security component deals with sensitive exceptions. These exceptions end up as authentication errors on the screen of the user. But at this point, there is an alternative message - not the original exception message - that gets shown to the user. Because these security-related exceptions usually contain sensitive information, useful for intruders, the exceptions have a special API (see the base class AuthenticationException).

These exception classes have the standard Exception constructor, but they also have extra methods:

/**
 * Message key to be used by the translation component.
 *
 * @return string
 */
public function getMessageKey()
{
    // ...  
}

/**
 * Message data to be used by the translation component.
 *
 * @return array
 */
public function getMessageData()
{
    // ...
}

That way, you could use the Symfony translator to show a message that tells the user what's wrong, but without exposing sensitive details:

$safeMessge = $translator->translate(
    $exception->getMessageKey(), 
    $exception->getMessageData()
);

Nice!

This was in the back of my head, and I wanted something like this to replace App_Exception - the one exception to rule them all. So I defined an interface with just these two methods, getMessageKey() and getMessageData(), and allowed any exception in the project to implement it. This of course solves the issue of not being tied to a specific exception class, so you can easily create your own custom type of exceptions (which I find very useful, in particular when in unit-testing).

Heresy

"But, you're a dogmatic, a purist! You wouldn't want the exceptions from your domain layer to implement an interface aimed at translating!" Agreed, translating is an infrastructure/top layer concern. However, this is so nice, so helpful, and so very convenient. Besides, we're not actually translating here - we're only making our objects ready for translation. It makes a lot of sense to me. Also, I'm not a dogmatic, nor a purist ;)

By the way, this new type of exception will end up in the logs, but should not require the immediate attention of the development team (i.e. developers shouldn't be alerted about them). However, they shouldn't be completely ignored too. If some of these exceptions get thrown a lot, this is a useful sign that something about the UI can be improved to actually prevent these exceptions and offer the user a nicer experience.

PHP exceptions validation Domain-Driven Design
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Timothy Rourke

You encourage avoiding using domain exceptions thrown from an entity or aggregate root because in such a case a single exception thrown may obscure the fact that several other input validations will also fail in subsequent requests. In the case of enforcing invariants for a REST API, how does one meaningfully avoid this one-exception-at-a-time problem without duplicating domain invariants into the application layer? I've been exploring a variety of JSON schema validation tools, but I'm hesitant to encode every possible invariant into the JSON payload validation of a write request because this tracks too much mud around the house. Any ideas would be appreciated!

Matthias Noback

Hi Timothy,
The question is: would you really want to validate an entire REST request, or would it be okay to wait for things to go wrong and fixes issues one by one? If you still want to give a complete list of problems, then you could try to do only superficial validation ("does this look right?") and leave the more detailed validation ("is this value unique?", etc.) to the application service. All of this depends on who your client is; a machine, a frontend for your app? Maybe you can add some more validation to the frontend in that case?

Timothy Rourke

Thanks, I think you're right that considering who the client is makes a difference, since in a significant way this is a UX decision. Great insights as always, thanks so much!

iophaer

Well put article

Matthias Noback

Thanks for letting me know!

osrd

How did you name the interface with the two methods?

Matthias Noback

"UserErrorMessage" - but this may well change.

Quentin P.

Hello Matthias.
When you say "It's often an application service or domain-level exception that's meant to reach the user" what do you really mean ?
Do you have an example ? It is about concurrency ?

Matthias Noback

It's not about concurrency. It's like, some things you can validate in the controller, but sometimes you'll only know what's wrong, until you load your entities and start calling methods on them. That's where you're inside an application service or entity, and would still like to talk back to the user.

Quentin P.

If you catch a specific Exception to talk back to the user that means you know that it could happen so why don't you validate before accessing the domain ?
Except concurrency I really don't know how it is useful.

Quentin P.

Ok it makes sense now @nezisi:disqus, thanks !

Matthias Noback

That's the thing, some things you can easily check in the UI/controller, but some things only turn out to be invalid/not allowed/etc. later on. Of course, you can check everything before accessing the domain layer, but this often has a bad effect on the quality of your domain objects. The knowledge about what is possible and what not ends up in the UI or the web controller or the validator tool that you use. This results in a lack of cohesion and encapsulation.

Troy P.

Are you suggesting that you believe all error conditions can be checked up front in say the controller -level beforehand?

Nezisi

I think he wants to say: "It is possible, but the necessary steps lead to cluttered code".

If you think of buying huge quantities of an article, which should be shipped in one or many containers, you need to calculate the volume of the container to give an estimate of how many containers you would get.

This could be trivial if you are the sole buyer - but If you add more buyers and the wish to "mix" several articles in one container for better capacity utilization, it get's more and more tricky: Different suppliers, maybe incompatible packaging, maybe it is not allowed to mix the articles in a container due to regulation and laws... a lot of logic can be involved here or none...

And this is the typical case where such an Exception can make sense. You could try to go down the rabbit hole and prevent everything, but in some cases it is just not desirable (e.g. the time necessary to validate twice, once on saving, second the time the user changes).

Nice article. Good idea.

Matthias Noback

Well put, thanks.

Petar Petrov (Sinethar)

Nicely put !