How Do You Handle 400 Bad Request in Angular?
1. What is 400 Bad Request?
HTTP 400 Bad Request means the server received the request, but the request is invalid and cannot be processed.
In an Angular + ASP.NET Core application, a common example is DTO/model validation failure.
Angular
↓
POST /api/products
↓
ASP.NET Core Web API
↓
Validate Product DTO
↓
Invalid data
↓
400 Bad Request
↓
Angular
↓
Display validation errors
For example, Angular sends:
{
"name": "",
"price": -100
}
The API may reject it because:
Nameis requiredPricemust be greater than0
2. Full Simple Sample Program
We'll use Angular with an ASP.NET Core Web API that returns validation errors.
ASP.NET Core — CreateProductDto.cs
using System.ComponentModel.DataAnnotations;
public class CreateProductDto
{
[Required(ErrorMessage = "Product name is required.")]
public string Name { get; set; } = string.Empty;
[Range(1, 1000000, ErrorMessage = "Price must be greater than 0.")]
public decimal Price { get; set; }
}
ASP.NET Core — ProductsController.cs
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpPost]
public IActionResult Create(CreateProductDto product)
{
return Ok(new
{
message = "Product created successfully.",
product
});
}
}
Because [ApiController] is used, ASP.NET Core automatically returns 400 Bad Request when model validation fails.
Angular
product.ts
export interface CreateProduct {
name: string;
price: number;
}
product.service.ts
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { CreateProduct } from './product';
@Injectable({
providedIn: 'root'
})
export class ProductService {
private http = inject(HttpClient);
private apiUrl =
'https://localhost:7001/api/products';
createProduct(product: CreateProduct): Observable<any> {
return this.http.post(
this.apiUrl,
product
);
}
}
error.interceptor.ts
import {
HttpErrorResponse,
HttpInterceptorFn
} from '@angular/common/http';
import {
catchError,
throwError
} from 'rxjs';
export const errorInterceptor: HttpInterceptorFn =
(req, next) => {
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 400) {
console.error(
'Bad Request:',
error.error
);
}
return throwError(() => error);
})
);
};
app.config.ts
import {
ApplicationConfig
} from '@angular/core';
import {
provideHttpClient,
withInterceptors
} from '@angular/common/http';
import {
errorInterceptor
} from './error.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([
errorInterceptor
])
)
]
};
products.ts
import {
Component,
inject
} from '@angular/core';
import {
ProductService
} from './product.service';
import {
CreateProduct
} from './product';
@Component({
selector: 'app-products',
standalone: true,
templateUrl: './products.html'
})
export class Products {
private productService =
inject(ProductService);
validationErrors: string[] = [];
message = '';
createProduct() {
this.validationErrors = [];
this.message = '';
const product: CreateProduct = {
name: '',
price: -100
};
this.productService
.createProduct(product)
.subscribe({
next: () => {
this.message =
'Product created successfully.';
},
error: (error) => {
if (
error.status === 400 &&
error.error?.errors
) {
const errors =
error.error.errors;
this.validationErrors =
Object.values(errors)
.flat() as string[];
}
}
});
}
}
products.html
<h2>Create Product</h2>
<button (click)="createProduct()">
Create Product
</button>
@if (message) {
<p>
{{ message }}
</p>
}
@if (validationErrors.length > 0) {
<h3>Validation Errors</h3>
<ul>
@for (
error of validationErrors;
track error
) {
<li>
{{ error }}
</li>
}
</ul>
}
3. How Does It Work?
Angular sends:
{
"name": "",
"price": -100
}
ASP.NET Core validates:
[Required]
public string Name { get; set; }
[Range(1, 1000000)]
public decimal Price { get; set; }
Both values are invalid.
The API returns approximately:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Name": [
"Product name is required."
],
"Price": [
"Price must be greater than 0."
]
}
}
Angular receives this as an HttpErrorResponse.
4. Accessing the 400 Error
You can check:
if (error.status === 400) {
}
The important difference is:
error.status
contains:
400
while:
error.error
contains the response body returned by ASP.NET Core.
So:
console.log(error.status);
might show:
400
and:
console.log(error.error.errors);
might contain:
{
"Name": [
"Product name is required."
],
"Price": [
"Price must be greater than 0."
]
}
5. Extracting Validation Messages
ASP.NET Core validation errors are normally grouped by property.
const errors = error.error.errors;
For example:
{
Name: [
'Product name is required.'
],
Price: [
'Price must be greater than 0.'
]
}
We can convert all messages into a single array:
this.validationErrors =
Object.values(errors)
.flat() as string[];
Result:
[
'Product name is required.',
'Price must be greater than 0.'
]
Then display them:
@for (error of validationErrors; track error) {
<li>
{{ error }}
</li>
}
6. Should 400 Be Handled Globally?
This is important.
You can detect/log 400 globally, but validation errors are usually better handled locally by the component/form.
Why?
Because the component knows how those errors should be presented.
For example:
POST /products
400
↓
Name is required
Price is invalid
The Product form should display those errors.
Another API could return:
POST /users
400
↓
Email is required
Password is invalid
The User form should display them.
Therefore:
400 Bad Request
↓
Error Interceptor
↓
Log if needed
↓
throwError()
↓
Component
↓
Display validation errors
7. Global vs Local Handling
A practical design is:
| Status | Typical handling |
|---|---|
400 |
Component/form |
401 |
Global interceptor |
403 |
Global interceptor |
404 |
Often local, depending on request |
409 |
Usually local/business-specific |
500 |
Global interceptor |
503 |
Global interceptor |
For example, the interceptor:
if (error.status === 400) {
console.error('Bad Request:', error.error);
}
return throwError(() => error);
Then the component:
error: (error) => {
if (
error.status === 400 &&
error.error?.errors
) {
this.validationErrors =
Object.values(error.error.errors)
.flat() as string[];
}
}
This separation is cleaner.
8. Handling a Simple 400 Message
Not every 400 response is an ASP.NET Core validation dictionary.
The API might explicitly return:
return BadRequest(new
{
message = "Product code already exists."
});
Angular receives:
{
"message": "Product code already exists."
}
Then handle it with:
error: (error) => {
if (error.status === 400) {
this.message =
error.error?.message
?? 'Invalid request.';
}
}
9. Two Types of 400 Responses
It's useful to distinguish these.
Validation error
{
"status": 400,
"errors": {
"Name": [
"Product name is required."
]
}
}
Handle:
error.error.errors
Business/request error
{
"message": "Invalid product information."
}
Handle:
error.error.message
A more robust component can support both:
error: (error) => {
if (error.status !== 400) {
return;
}
if (error.error?.errors) {
this.validationErrors =
Object.values(error.error.errors)
.flat() as string[];
}
else if (error.error?.message) {
this.validationErrors = [
error.error.message
];
}
else {
this.validationErrors = [
'Invalid request.'
];
}
}
10. Angular Client Validation vs API Validation
Suppose your Angular form has:
name: ['', Validators.required]
Angular can stop obviously invalid input before making the API request.
But Angular validation is not a security boundary.
A user can bypass Angular and directly call:
Postman
↓
ASP.NET Core API
Therefore validation should exist on the server too.
Angular Validation
↓
Better user experience
ASP.NET Core Validation
↓
Authoritative server-side validation
Key Points
400 Bad Requestmeans the server considers the request invalid.- A very common cause is DTO/model validation failure.
- ASP.NET Core
[ApiController]can automatically return400for invalid model state. - Angular receives HTTP failures as
HttpErrorResponse. - Check the status using:
error.status === 400
- The backend response body is available through:
error.error
- ASP.NET Core validation details are commonly available through:
error.error.errors
- Global interceptors can detect/log
400, but form validation errors are generally better displayed by the relevant component. - Client-side validation improves UX, but server-side validation is still mandatory.
- Do not assume every
400response has the same JSON structure; define a consistent API error contract where possible.
Interview Questions and Answers
1. What does 400 Bad Request mean?
It means the server received the HTTP request but considers it invalid, commonly because of malformed input, DTO validation failure, or invalid request data.
2. How do you handle 400 Bad Request in Angular?
Check HttpErrorResponse.status and inspect the response body.
if (error.status === 400) {
console.log(error.error);
}
For validation failures, extract the validation messages and display them to the user.
3. How do you get ASP.NET Core validation errors in Angular?
Typically:
error.error.errors
For example:
const errors = error.error.errors;
this.validationErrors =
Object.values(errors).flat() as string[];
4. Should 400 errors be handled globally?
Common logging can be global, but form/validation-specific 400 errors are usually handled locally because the component knows how to display them correctly.
5. What is the difference between Angular validation and API validation?
Angular validation provides immediate client-side UX feedback, while API validation is the authoritative server-side validation and cannot be safely replaced by client validation.
6. Does [ApiController] automatically return 400?
Yes. In ASP.NET Core, controllers using [ApiController] automatically return a 400 response when model validation fails under the standard API behavior.
7. What is the most important property of HttpErrorResponse for checking the HTTP status?
error.status
Example:
if (error.status === 400) {
// Handle bad request
}
8. What is the difference between error.status and error.error?
error.status
contains the HTTP status code, such as 400.
error.error
contains the response body returned by the backend.
So, for ASP.NET Core validation:
error.status → 400
error.error.errors → Validation messages
Interview Quick Answer
How do you handle
400 Bad Requestin Angular?
I handle a 400 Bad Request using HttpErrorResponse. I check error.status === 400 and inspect error.error for the response body. For ASP.NET Core validation errors, I extract error.error.errors and display the relevant validation messages in the form. Global interceptors can log 400 errors, but form-specific validation is usually handled in the component.