Validation and error handling in AngularJS applications

In this article we are going to be more concrete and provide more extensive information on the validation and error handling in AngularJS applications.
What do we need
AngularJS is SPA framework. Such SPAs (single page applications) are expected to be highly responsive which means, among other, solid front-end validation. But most of AngularJS apps communicate with back-end apps and must be ready to process server-side validation errors too. Moreover, there are some common things that are required for good validation and error handling processing core. Here is the list of basical error functionality requirements for typical AngularJS app:
- input fields and forms must be validated on-the-fly before sending data to back-end app;
- error handling and common validation rules (e.g. required) must work in transparent way not dirtying controllers with validation code;
- custom validation logic (specific to particular page/widget/form) must fit common validation interface;
- back-end errors (if any) must fit common pattern too and must be transparent for end-users (look exactly like front-end validation errors);
- field and form errors must be shown in user-friendly and smart manner (e.g. do not show errors on untouched controls and fields, etc.).
Bad practice
In this section we are going to share an example of bad validation and error handling implementation. There are very few reasons to use such pattern, except you are beginner and have a lack of experience with AngularJS. So the most straightforward and brute-force validation approach is manual controller-based validation:

Implementation details may differ, nevertheless this validation approach has a bunch of shortcomings:
- all native HTML and AngularJS validation tools are ignored;
- all validation and error handling stuff is processed manually;
- controllers and templates are cluttered up with boring validation code and markup.
Never follow such patterns unless you are sure that any other approach can not be used.
Good practices
Out of the box
The cool thing about AngularJS is that it has built-in implementation of most needed validation and error handling features. All this functionality is embedded in form handling logic. So if you want to use built-in validation tools - then you have to:
- wrap all your input controls into form;
- name those controls (with name attribute);
- name the form.
After that form controller will expose number of validation methods and properties which are being automatically monitored and updated by AngularJS core. Each input has several properties that represent corresponding control states: $untouched/$touched, $pristine/$dirty, $valid/$invalid. Moreover form itself exposes similar properties: $pristine/$dirty, $valid/$invalid and $submitted. You can easily combine those properties in conditional expressions and use them with ng-class, ng-disabled and other directives to get required validation logic and UI effects:

Notice that controller is much thinner and doesn't contain any validation code any more! All logic is under the hood, validation rules are exposed in template with validators. AngularJS supports next validators out of the box:
- type - native HTML input type (e.g. number, email, etc.);
- required/ng-required - the only difference is that ng-required value can be changed dynamically;
- ng-minlength, maxlength/ng-maxlength - number of characters restrictions;
- pattern/ng-pattern - difference is again in dynamic nature.
This validators, including powerful ng-pattern which accepts regular expressions, are enought to satisfy most of front-end validation tasks.
Reusable custom validators
But sometimes you can face custom (project-specific) validation requirements. If such validator is to be used in multiple pages/widgets/controllers - then the best way to implement it is dedicated validator directive coupled with ngModel. The logic of such directives is pretty typical:
- directive requires ng-model which provides DOM listening link() function with model controller instance as fourth argument;
- in directive you define validation function that takes element value and returns boolean validity flag;
- finally you add defined validation function to validators list of ng-model controller.
Here is an example of such validation directive nameBlackList which accepts all strings except Barack and Donald:

Directive implementation can seem bit graceless, but using it will be piece of cake! Just add custom name-black-list attribute to the input element and AngularJS magic will do the rest:

One-occasion custom validators
Another custom validation use case is one-occasion verification. If you need to validate something really special in one particular widget/page/controller - then there is no sense to implement reusable directive and share it among all app components. The best solution here is to encapsulate validation function in particular controller. But this is not a reason to use brute-force ugly manual validation from our first example. We must adjust this custom function to fit standard validation pattern/interface.
Directive that takes validation function
The solution of one-occasion validation issue is pretty predictable: we need a directive (as always with AngularJS) which accepts custom verification function as input parameter. You can find example implementation below:

To use this directive you have to define in controller a verification function that takes element value and returns boolean validity flag:

And finally bind this function to the directive:

Of course this validation-function directive is primitive: it doesn't support interpolation (so you can't change verification function dynamically), it doesn't accept asynchronous requests, etc. You can use it as basis and improve according to project requirements.
ui-validate
But you don't have to necessarily implement one occasion validation function directive yourself because guys from AngularUI team have already done this in their ui-validate package. It is compact, flexible, well tested and maintained module. We highly recommend to use it in your projects. The directive is applied in very common way:

You can see that ui-validate usage is pretty similar to our custom directive above. But it has a lot of additional features, e.g. support of promise-returning functions, etc.
Processing server-side validation results
While front-end validation improves user experience drastically (thanks to instant app feedback on user input), it is not completely trusty (because the code runs on user side) and reliable (because data is usually stored on back-end and AngularJS app simply can't process all validation cases like occupied login name, etc.). Thus most SPA apps have to support back-end validation. According to our experience the best solution of this problem is helper service that takes form controller context along with error response and sets corresponding error flags (which of cource must fit standard AngularJS validation pattern). Implementation of the service is closely linked to error response structure, therefore it varies from project to project. But core logic is always the same, see example below:

Notice few key points in the above setting server errors helper:
- back-end errors are marked as serverError in controls and form itself
- method checks if there are non-field (form-level) server errors and makes form invalid if any
- method searches controls with specified in error response names and makes them invalid
But once we set serverError - then it will always stay enabled and never get reset back itself, however server errors must be reset after user makes any updates to the control. To accomplish this goal we use special directive, see below:

In a nutshell the server-validation directive does next:
- requires to be attached to form (this provides form controller to link() function as fourth attribute)
- defines a reset function that disables serverError on control and containing form
- watches form controller instance updates and checks if new controls were added to the form, this is needed for dynamically added (with ng-if and other DOM directives) form controls
- adds reset function to validator list of all form controls which guarantees serverError reset after any control updates (both view and model)
Here is use example of this directive (notice how form is bound to controller with name="$ctrl.form6" statement):

Error styles and messages
CSS validation classes
AngularJS automatically adds and removes CSS classes to form controls and form itself depending on current validation state. Class names correspond to control validation properties names: ng-dirty, ng-valid, ng-touched, etc. Furthermore, there are a number ng-valid-validatorname/ng-invalid-validatorname class pairs which correspond to every validator defined for the control (these can be used for custom styling of some specific errors).
In fact you have two main options to define error styles:
- dynamically add custom CSS classes to elements with ng-class directive depending on control validation properties (e.g. ng-class="{'has-error': $ctrl.formName.fieldName.$touched && $ctrl.formName.fieldName.$invalid}")
- define style rules in CSS (LESS, SASS, etc.) files, that are bound to standard AngularJS validation classes (e.g. .ng-invalid.ng-touched {background-color: red;})
The second option is more preferable, because it keeps templates much cleaner.
In our above examples we disabled save button to prevent user from submission when form is invalid. It's not the best approach for sure. You are free to implement any validation scenario you need. But in most cases we prefer next approach:
- allow user to submit form even if form is invalid;
- if user hasn't tried to submit form - then do not show errors on control unless user touched and left it;
- if user has tried to submit form - then show errors on all invalid controls and form itself.
Form controller exposes very useful property $submitted along with ng-submitted class and $setSubmitted() method. And AngularJS implementation of above validation approach can look like this:
- ng-class="{'has-error': $ctrl.formName.fieldName.$invalid && ($ctrl.formName.fieldName.$touched || $ctrl.formName.$submitted)}"
- .ng-invalid.ng-touched, .ng-submitted .ng-invalid {background-color: red;}
ng-messages
Highlighting invalid controls is not enought for good validation core. You need to show somehow error notifications so the user will know what is wrong. The best way to do this in AngularJS app is ng-messages directive. Here is self-explaining example:

Here are some important points:
- ng-model controller of each control contains $error object;
- this $error object contains properties (keys) for every failing validator;
- control's $error object is used as data source for ng-messages directive with error messages;
- each validator must have error message (BTW, one error message can be coupled with several validators, e.g. ng-message="pattern, required");
- error messages are shown only if field is touched or containing form is submitted;
- error message appear if the value of ng-message attribute is existing key in $error object;
- by default only one message is shown, this can be changed with multiple attribute;
- top messages have higher priority, server error must have highest priority.
Common tips as conclusion
- Always use forms. AngularJS app can be built without forms: you simply define models in controller, declare inputs in template, couple them together and fire server-side request on some action. But forms provide you with additional validation tools and make your app work in more natural way. BTW, if you need to implement complex components with nested and dynamical forms - then consider replacing regular forms with ng-form directives.
- Add novalidate attribute to the form. This will disable standard browser validation (as well as browser native validation popover messages). You will not need it since AngularJS takes care of validation.
- Always give names to inputs in your template (we mean HTML name attribute). Later you can address those inputs with given name inside your controller and template.
- Input name and controller model name must be equal to name of corresponding field in back-end response. Sometimes your app works with server API that follows different from camel case naming convention (e.g. Django REST framework which follows underscore like other Python apps). In this case you may want to rename fields in controller response/request handlers (or in more elegant way with service's transformResponse/transformRequest functions). But sooner or later this will cause confusion and mess. Better create an exception in configuration of project linter (if any) and use original names everywhere.
- Make with your back-end team a convention about server-side error response structure and always follow this convention. All your response error handlers through the whole app must be the same. Reject and claim for refactoring of each non-standard back-end error response unless it has significant reasons (e.g. usage of 3-rd party validation services which can not be changed). Such cases must be exception but not rule!
- Do not forget about markup for non-field (form-level) errors in your templates, or replace this markup with toaster notification service like AngularJS-Toaster.
- Try to avoid complex all-in-one 3-rd party validation packages. E.g. if you google AngularJS validation - then you will find angular-validation in top search results. The package provides multipurpose validation toolset (service, directives, message system, etc.). It is very tempting to attach such package to your app and just use it not taking care about things under the hood. But according to our experience, for small app with simple validation logic such packages are overkill. AngularJS has awesome build-in validation core which is more than enough for those cases. And if you need complex validation - then you are likely to fall into troubles with such packages trying to adopt them to your app peculiarity.
- Do not hesitate to use specialized single purpose packages that support native AngularJS validation interfaces and provide validation logic which is difficult to implement and maintain (e.g. angular-credit-cards).
- Keep calm and ignore React hype.