Forms are a crucial part of any web application. They collect user input and are the primary interface for interacting with the application. However, simply capturing user input is not enough. It is equally important to validate the input and provide clear, actionable feedback to users when something is incorrect.
Angular provides a powerful form handling system that makes form validation and error display both flexible and user-friendly. Properly displaying form errors improves user experience, reduces mistakes, and ensures the integrity of the data submitted.
In this guide, we will explore how to display form errors in Angular for both template-driven and reactive forms. We will cover techniques for accessing errors, showing user-friendly messages, handling dynamic validation, nested forms, and best practices for building robust forms.
Why Display Form Errors?
Displaying form errors is not just about aesthetics—it’s essential for:
- Guiding the user: Helps users correct mistakes in real-time.
- Preventing invalid submissions: Reduces the likelihood of sending bad data to the server.
- Improving accessibility: Clear error messages help users understand the requirements.
- Providing instant feedback: Users can see mistakes as they type or when they leave a field.
Without proper error display, users might get frustrated, abandon the form, or submit incorrect data.
Form Errors in Angular
In Angular, every form control has an errors
property. This property is an object that contains the validation errors for that control. Each key corresponds to a specific validator that failed.
For example:
{
required: true,
minlength: { requiredLength: 3, actualLength: 1 }
}
required
indicates that the field was left empty.minlength
shows the minimum length required versus the actual length entered.
Accessing these errors allows developers to display meaningful messages in the UI.
Displaying Errors in Reactive Forms
Reactive forms provide a programmatic approach to form handling, making error access straightforward.
Example: Basic Reactive Form
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
@Component({
selector: 'app-user-form',
templateUrl: './user-form.component.html'
})
export class UserFormComponent implements OnInit {
userForm!: FormGroup;
ngOnInit() {
this.userForm = new FormGroup({
username: new FormControl('', [Validators.required, Validators.pattern(/^[a-zA-Z0-9]*$/)])
});
}
onSubmit() {
if (this.userForm.valid) {
console.log('Form submitted:', this.userForm.value);
} else {
console.log('Form is invalid');
}
}
}
Template: Displaying Errors
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<label for="username">Username:</label>
<input id="username" formControlName="username" />
<div *ngIf="userForm.get('username')?.errors?.required && userForm.get('username')?.touched">
Username is required
</div>
<div *ngIf="userForm.get('username')?.errors?.pattern && userForm.get('username')?.touched">
Only letters and numbers allowed
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
Key Points:
- Use
userForm.get('username')?.errors
to access the errors object. - Check for
touched
ordirty
to avoid showing errors before the user interacts with the field. - Provide specific messages for each validator.
Displaying Multiple Errors
A single form control can fail multiple validations. For example, the username might be required and have a minimum length. Angular allows you to display multiple messages simultaneously.
<div *ngIf="userForm.get('username')?.touched">
<div *ngIf="userForm.get('username')?.errors?.required">Username is required</div>
<div *ngIf="userForm.get('username')?.errors?.minlength">
Minimum length is {{ userForm.get('username')?.errors?.minlength.requiredLength }}
</div>
<div *ngIf="userForm.get('username')?.errors?.pattern">Only letters and numbers allowed</div>
</div>
- Combine multiple
*ngIf
directives to display all relevant messages. - Use interpolation to show dynamic values like required length.
Displaying Errors in Template-Driven Forms
Template-driven forms use directives in the HTML template to handle validation and errors. Access the control using a template reference variable.
Example: Template-Driven Form
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
<label for="username">Username:</label>
<input id="username" name="username" ngModel required pattern="[a-zA-Z0-9]*" #username="ngModel" />
<div *ngIf="username.errors?.required && username.touched">
Username is required
</div>
<div *ngIf="username.errors?.pattern && username.touched">
Only letters and numbers allowed
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
- Use
#username="ngModel"
to reference the control. - Access
errors
on the control directly to display messages.
Using Functions to Simplify Error Display
For complex forms with many fields, defining error messages in the template can become repetitive. A helper function can centralize error messages.
Example: Error Helper Function
getErrorMessage(controlName: string): string {
const control = this.userForm.get(controlName);
if (!control || !control.errors) return '';
if (control.errors.required) return 'This field is required';
if (control.errors.minlength) {
return Minimum length is ${control.errors.minlength.requiredLength}
;
}
if (control.errors.maxlength) {
return Maximum length is ${control.errors.maxlength.requiredLength}
;
}
if (control.errors.pattern) return 'Invalid format';
return '';
}
Template Usage
<div *ngIf="userForm.get('username')?.touched">
{{ getErrorMessage('username') }}
</div>
- Reduces template clutter and improves maintainability.
- Centralizes error handling logic for easier updates.
Conditional Error Display
Sometimes errors should only display after the user tries to submit the form.
submitted = false;
onSubmit() {
this.submitted = true;
if (this.userForm.valid) {
console.log('Form submitted:', this.userForm.value);
}
}
<div *ngIf="(userForm.get('username')?.touched || submitted) && userForm.get('username')?.errors?.required">
Username is required
</div>
- Combines
touched
andsubmitted
states for more intuitive error display. - Avoids showing errors too early.
Dynamic Forms and Nested Form Groups
For large or dynamic forms, errors can be displayed for nested controls using form groups.
Example: Nested Form Group
this.userForm = new FormGroup({
username: new FormControl('', Validators.required),
address: new FormGroup({
street: new FormControl('', Validators.required),
city: new FormControl('', Validators.required)
})
});
Template: Nested Errors
<div formGroupName="address">
<label for="street">Street:</label>
<input id="street" formControlName="street" />
<div *ngIf="userForm.get('address.street')?.errors?.required && userForm.get('address.street')?.touched">
Street is required
</div>
<label for="city">City:</label>
<input id="city" formControlName="city" />
<div *ngIf="userForm.get('address.city')?.errors?.required && userForm.get('address.city')?.touched">
City is required
</div>
</div>
- Use dot notation
userForm.get('address.city')
to access nested controls. - Maintain consistent error handling for complex forms.
Best Practices for Displaying Form Errors
- Always show meaningful messages – Avoid technical jargon.
- Use
touched
ordirty
– Prevent errors from showing before user interaction. - Combine with
submitted
state – Useful for submit-time validation. - Centralize error messages – Use functions or objects for maintainability.
- Style errors clearly – Highlight fields with red borders or icons for visibility.
- Handle nested forms consistently – Maintain readability in large forms.
- Avoid repetitive templates – Use helper functions for multiple fields.
Example: Complete Reactive Form with Error Display
this.userForm = new FormGroup({
username: new FormControl('', [Validators.required, Validators.pattern(/^[a-zA-Z0-9]*$/)]),
email: new FormControl('', [Validators.required, Validators.email]),
password: new FormControl('', [Validators.required, Validators.minLength(6)]),
address: new FormGroup({
street: new FormControl('', Validators.required),
city: new FormControl('', Validators.required)
})
});
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<input formControlName="username" placeholder="Username" />
<div *ngIf="userForm.get('username')?.errors?.required && userForm.get('username')?.touched">
Username is required
</div>
<div *ngIf="userForm.get('username')?.errors?.pattern && userForm.get('username')?.touched">
Only letters and numbers allowed
</div>
<input formControlName="email" placeholder="Email" />
<div *ngIf="userForm.get('email')?.errors?.required && userForm.get('email')?.touched">
Email is required
</div>
<div *ngIf="userForm.get('email')?.errors?.email && userForm.get('email')?.touched">
Invalid email
</div>
<div formGroupName="address">
<input formControlName="street" placeholder="Street" />
<div *ngIf="userForm.get('address.street')?.errors?.required && userForm.get('address.street')?.touched">
Street is required
</div>
<input formControlName="city" placeholder="City" />
<div *ngIf="userForm.get('address.city')?.errors?.required && userForm.get('address.city')?.touched">
City is required
</div>
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
Leave a Reply