Validation is the way we guarantee a system’s integrity and consistency, therefore, validation is found in almost every piece of software no matter how tiny or large.
This is the first in a series of articles about validation, click follow to get updates about coming articles!
From a validation perspective, we can break most software systems into 3 major parts: Use-Cases, Entities and builders, and Datastores.
Use-case is a usage scenario for your entities and other system components to satisfy a business need, therefore a use-case is the most context-aware component of your system because it holds the truth about the user intent for using the system in a certain way.
we can specify all the validation a use-case needs without relying on any other part in the system, but some of these rules are not for any use-case to violate, rather they span many use-cases and scenarios, these are called “Invariants”.
Entity and Invariants
an invariant is a set of assertions that must always hold true during the life of an object for the program to be valid.
Therefore, it’s better to encapsulate these in their related objects which guarantees that any client code that uses these objects will accordingly be enforcing these rules, therefore invariants are crosscutting in your application, we call objects that hold the invariant business rules “Entities”.
some rules are of another nature like concurrency issues and state persistence can be — in some cases — delegated to the Datastore, but a data store has to be more tolerant than a use-case or an entity about the validations it can apply, Datastores merely can do any help in regard of validation, because it has to make room for the variance in entities state (like nullable fields for example).
Validation and Error reporting
While validation is crucial to almost all systems but it also can be harmful if not correctly applied, for instance, incomplete error feedback from the back-end can harm your user experience, also the non-uniformity error handling and reporting can result in a buggy system or at best one that is hard to debug.
Exceptions vs Custom errors
In my experience, I encountered 2 ways for validation, either by throwing
“Exceptions” or returning “Validation Errors” and both have some serious disadvantages. in this article, I want to introduce you to a third pattern that combines the best in both.
Method #1: Using Exceptions
As you can see in the above example the thrown `Exception` will break the continuity of code execution, they will not allow for further error handling, which makes error reporting very cumbersome and doesn’t allow for collecting more errors.
We should not return error by error on each request as this can ruin the UX.
Imagine a user seeing an error for a “short name”, then after fixing and re-submitting we send him another error about an “illegal character” on the same field. that would be frustrating.
- prevents the construction of Invalid Entities.
- Breaks code execution continuity.
- Returning error by error can break the user experience.
Method #2: Using Custom Errors
we can use some sort of error collection mechanism in our methods and use the bag of errors as the return value for our methods that do the validation which solves the problem of breaking the continuity of code execution.
At the first glance returning a collection of errors, might seems like a good idea, But as you can see, on the other hand, this will pollute your design making most of the return types of type
ErrorBag as too many methods will have some validation logic in it.
this alone will prevent you from using meaningful return types, preventing your interfaces from revealing the real intent behind them which will, in turn, make the design in worse condition.
but let’s have another look at the client code as well:
const errBag1 = x.setAge(16); yes, we are returning an error bag but still, we are relying on the client developer being vigilant enough to use these return types as he/she can easily dismiss the returned value resulting in an invalid entity that can be stored and operated on, which in turn violates the principle of always valid entities.
Read more about always-valid entities by Greg Young: here.
- A nicer way of returning errors to the user.
- possible dismissal of the returned errors will result in invalid entities being processed.
- strongly harms the interface design.
Method #3: Exception Aggregation
The proposed solution here is What I call “Exception Aggregation” — IDK if this is a good name, which is a pattern that combines both solutions, reporting using exceptions (without breaking)* the code execution continuity.
AggregateException , a class that itself inherits from the
Exception base class (in typescript it's
Error class but I use
Exception here not to be confused with custom errors solution for non-typescript users).
And the client code will look something like this:
or even better:
While this obviously requires more keystrokes than normal flow, but this simple pattern forces the client to proactively handle Exceptions by aggregating them, without breaking the execution continuity also without sacrificing the entities’ interface cleanliness or communicating its intent.
- Forces the client to proactively handle Exceptions.
- It does not sacrifice your design.
- A nicer way of returning errors to the user.
- requires the usage of closures.
NOTE 1: the best place for this pattern to be used is where you construct or reconstruct your entities like Factories, Builders, …etc, where errors will be proactively handled, where you are able to control whether to return a valid entity or throw the Aggregated Exceptions.
“Exception Aggregation” is a good way to handle errors without breaking the continuity of code execution as it combines the best of both worlds without sacrificing your design or breaking user experience.
This pattern can be combined with mappers to loop through the errors in the Aggregate Exception and return more descriptive validation errors for the client-side apps.
There is much more to say about validation, but I wanted to start with introducing this pattern as it has proven useful.
If you find this article useful, please share it and follow for more articles like this.