Exceptions and talking back to the user
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.