How Should Angular Consume ASP.NET Core ProblemDetails Responses?
1. What is ProblemDetails?
ProblemDetails is a standard HTTP API error response format based on RFC 7807 / the newer RFC 9457 specification.
ASP.NET Core can use it to return a consistent error structure such as:
{
"type": "https://example.com/errors/product-not-found",
"title": "Product not found",
"status": 404,
"detail": "Product with ID 100 was not found.",
"instance": "/api/products/100"
}
Instead of Angular having to handle unrelated error shapes such as:
{
"message": "Not found"
}
or:
{
"errorMessage": "Product not found"
}
you can establish a consistent contract:
ASP.NET Core
↓
ProblemDetails
↓
HTTP Response
↓
Angular HttpErrorResponse
↓
error.error
↓
ProblemDetails object
↓
Display / process error
2. Full Simple Sample Program
We'll create:
ASP.NET Core Web API
↓
ProblemDetails
↓
Angular
↓
HttpErrorResponse
↓
ProblemDetails interface
↓
Component
ASP.NET Core — Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseExceptionHandler();
app.MapControllers();
app.Run();
ASP.NET Core — ProductsController.cs
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id:int}")]
public IActionResult Get(int id)
{
if (id == 100)
{
return Problem(
statusCode: StatusCodes.Status404NotFound,
title: "Product not found",
detail: $"Product with ID {id} was not found."
);
}
return Ok(new
{
Id = id,
Name = "Laptop",
Price = 50000
});
}
}
If we request:
GET /api/products/100
ASP.NET Core returns a ProblemDetails response similar to:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
"title": "Product not found",
"status": 404,
"detail": "Product with ID 100 was not found.",
"instance": "/api/products/100"
}
Angular
problem-details.ts
Create an interface matching the standard response.
export interface ProblemDetails {
type?: string;
title?: string;
status?: number;
detail?: string;
instance?: string;
[key: string]: unknown;
}
The last line:
[key: string]: unknown;
allows ASP.NET Core to include extensions such as:
{
"traceId": "00-abcd1234..."
}
product.ts
export interface Product {
id: number;
name: string;
price: number;
}
product.service.ts
import { inject, Injectable } from '@angular/core';
import {
HttpClient
} from '@angular/common/http';
import {
Observable
} from 'rxjs';
import {
Product
} from './product';
@Injectable({
providedIn: 'root'
})
export class ProductService {
private http = inject(HttpClient);
private apiUrl =
'https://localhost:7001/api/products';
getProduct(id: number): Observable<Product> {
return this.http.get<Product>(
`${this.apiUrl}/${id}`
);
}
}
products.ts
import {
Component,
inject
} from '@angular/core';
import {
HttpErrorResponse
} from '@angular/common/http';
import {
ProductService
} from './product.service';
import {
Product
} from './product';
import {
ProblemDetails
} from './problem-details';
@Component({
selector: 'app-products',
standalone: true,
templateUrl: './products.html'
})
export class Products {
private productService =
inject(ProductService);
product?: Product;
errorMessage = '';
loadProduct() {
this.errorMessage = '';
this.productService
.getProduct(100)
.subscribe({
next: product => {
this.product = product;
},
error: (error: HttpErrorResponse) => {
const problem =
error.error as ProblemDetails;
this.errorMessage =
problem.detail
?? problem.title
?? 'Something went wrong.';
}
});
}
}
products.html
<h2>Product</h2>
<button (click)="loadProduct()">
Load Product
</button>
@if (product) {
<div>
<h3>
{{ product.name }}
</h3>
<p>
Price: {{ product.price }}
</p>
</div>
}
@if (errorMessage) {
<div>
{{ errorMessage }}
</div>
}
3. How Does It Work?
Suppose Angular calls:
GET /api/products/100
ASP.NET Core cannot find the product.
It returns:
{
"title": "Product not found",
"status": 404,
"detail": "Product with ID 100 was not found."
}
Angular's HttpClient treats a 404 response as an error.
Therefore this executes:
error: (error: HttpErrorResponse) => {
}
The important distinction is:
HttpErrorResponse
|
+--- status
|
+--- message
|
+--- url
|
+--- error
|
+--- ProblemDetails
So:
error.status
is:
404
while:
error.error
contains:
{
"title": "Product not found",
"status": 404,
"detail": "Product with ID 100 was not found."
}
4. Create a TypeScript ProblemDetails Interface
Instead of repeatedly using:
error.error.title
error.error.detail
error.error.status
define a TypeScript model:
export interface ProblemDetails {
type?: string;
title?: string;
status?: number;
detail?: string;
instance?: string;
[key: string]: unknown;
}
Then:
const problem =
error.error as ProblemDetails;
Now:
problem.title
problem.detail
problem.status
problem.instance
problem.type
are easier to work with.
5. Understanding Each ProblemDetails Property
A standard Problem Details object has these core members:
| Property | Purpose |
|---|---|
type |
URI identifying the problem type |
title |
Short human-readable summary |
status |
HTTP status code |
detail |
Human-readable explanation for this occurrence |
instance |
Identifies the specific occurrence/request |
Example:
{
"type": "https://example.com/errors/product-not-found",
"title": "Product not found",
"status": 404,
"detail": "Product 100 does not exist.",
"instance": "/api/products/100"
}
6. title vs detail
This is important.
title
General description:
"title": "Product not found"
detail
Specific information about this occurrence:
"detail": "Product with ID 100 was not found."
Therefore Angular might prefer:
this.errorMessage =
problem.detail
?? problem.title
?? 'Something went wrong.';
7. Handle ProblemDetails Globally
You can also parse Problem Details in your global error interceptor.
error.interceptor.ts
import {
HttpErrorResponse,
HttpInterceptorFn
} from '@angular/common/http';
import {
inject
} from '@angular/core';
import {
Router
} from '@angular/router';
import {
catchError,
throwError
} from 'rxjs';
import {
ProblemDetails
} from './problem-details';
export const errorInterceptor: HttpInterceptorFn =
(req, next) => {
const router = inject(Router);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
const problem =
error.error as ProblemDetails;
console.error(
'API Problem:',
problem
);
switch (error.status) {
case 401:
router.navigate(['/login']);
break;
case 403:
router.navigate(['/unauthorized']);
break;
case 500:
router.navigate(['/server-error']);
break;
}
return throwError(() => error);
})
);
};
Notice:
return throwError(() => error);
We preserve the original HttpErrorResponse.
This allows the component to continue checking:
error.status
and:
error.error
8. What About ASP.NET Core Validation Errors?
This is where an important distinction appears.
ASP.NET Core model validation commonly returns ValidationProblemDetails, which extends the Problem Details concept with an errors dictionary.
Example:
{
"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."
]
}
}
So Angular can create another interface.
validation-problem-details.ts
import {
ProblemDetails
} from './problem-details';
export interface ValidationProblemDetails
extends ProblemDetails {
errors: {
[key: string]: string[];
};
}
Now you have:
ProblemDetails
↑
|
ValidationProblemDetails
Similar conceptually to ASP.NET Core.
9. Consuming ValidationProblemDetails
error: (error: HttpErrorResponse) => {
if (
error.status === 400 &&
error.error?.errors
) {
const validationProblem =
error.error as ValidationProblemDetails;
console.log(
validationProblem.errors
);
}
}
Suppose:
validationProblem.errors
contains:
{
Name: [
'Product name is required.'
],
Price: [
'Price must be greater than 0.'
]
}
You can then map those errors to Angular controls:
Object.entries(validationProblem.errors)
.forEach(([field, messages]) => {
const control =
this.productForm.get(
field.toLowerCase()
);
if (control) {
control.setErrors({
...control.errors,
serverError:
messages.join(' ')
});
}
});
10. Recommended Angular Models
For an Angular + ASP.NET Core application, keeping these two models is useful.
problem-details.ts
export interface ProblemDetails {
type?: string;
title?: string;
status?: number;
detail?: string;
instance?: string;
[key: string]: unknown;
}
validation-problem-details.ts
import {
ProblemDetails
} from './problem-details';
export interface ValidationProblemDetails
extends ProblemDetails {
errors: {
[key: string]: string[];
};
}
Then:
API Error
|
+-- ProblemDetails
|
+-- ValidationProblemDetails
11. ProblemDetails Extensions
ASP.NET Core can include additional information in ProblemDetails.
For example:
{
"title": "Unexpected server error",
"status": 500,
"detail": "An unexpected error occurred.",
"traceId": "00-abc123xyz-456..."
}
The traceId can be particularly useful for support and diagnostics.
Angular could retrieve it:
const problem =
error.error as ProblemDetails;
const traceId =
problem['traceId'];
Then you could display something like:
Something went wrong.
Reference ID:
00-abc123xyz-456...
The user can provide that reference to support, and the backend team can correlate it with logs.
For your ASP.NET Core applications, this also fits naturally with correlation-ID/logging patterns.
12. Don't Depend Only on HTTP Status
Suppose you have two different 404 conditions:
404 → Product not found
404 → Article not found
Checking only:
error.status === 404
tells you the HTTP category, but not necessarily the specific business problem.
A stable type URI can identify the problem more precisely:
{
"type": "https://api.example.com/problems/product-not-found",
"title": "Product not found",
"status": 404
}
Angular can inspect:
problem.type
For example:
if (
problem.type ===
'https://api.example.com/problems/product-not-found'
) {
// Product-specific handling
}
For machine-readable decisions, a stable problem type or an explicit extension such as an application error code is preferable to parsing the human-readable detail.
13. Don't Use detail as a Business Error Code
Avoid logic like:
if (
problem.detail ===
'Product does not exist'
) {
}
The backend wording could change:
Product does not exist
to:
The requested product was not found
and your Angular logic would break.
Instead, use a stable identifier such as:
{
"type": "https://api.example.com/problems/product-not-found",
"title": "Product not found",
"status": 404
}
or a custom extension:
{
"title": "Product not found",
"status": 404,
"errorCode": "PRODUCT_NOT_FOUND"
}
Then Angular can make decisions based on the stable identifier.
14. Global vs Local Handling
A clean design is:
ASP.NET Core
↓
ProblemDetails
↓
Angular HTTP Interceptor
↓
Inspect common errors
↓
┌──────────────┬──────────────┐
↓ ↓ ↓
401 403 500
↓ ↓ ↓
Login Forbidden Global error
400 / 404 / 409
↓
Pass to component/service
↓
Business-specific handling
For example:
Global interceptor
Handle:
401
403
generic 500
Component
Handle:
400 ValidationProblemDetails
404 Product not found
409 Product already exists
15. Recommended Architecture
For Angular + ASP.NET Core:
ASP.NET Core
|
Global Exception Handler
|
ProblemDetails
|
HTTP Response
↓
Angular HttpClient
↓
Error Interceptor
|
┌─────────┴─────────┐
↓ ↓
Common errors Business errors
↓ ↓
401/403/500 400/404/409
↓ ↓
Global UX Component/Form
This gives you a consistent error contract across the application.
Key Points
ProblemDetailsprovides a standardized API error representation.- ASP.NET Core has built-in support through
AddProblemDetails(). - Angular receives the Problem Details body through:
error.error
- Create a TypeScript
ProblemDetailsinterface rather than repeatedly using untyped properties. - ASP.NET Core validation failures commonly use
ValidationProblemDetails. - Model
ValidationProblemDetailsseparately with anerrorsdictionary. titleis a short summary.detaildescribes the specific occurrence.statuscontains the HTTP status represented by the problem response.typecan provide a stable machine-readable problem category.instanceidentifies the particular occurrence/resource.- Custom extensions such as
traceIdorerrorCodecan be useful. - Don't write business logic that depends on matching human-readable
detailtext. - Let the interceptor handle global HTTP concerns.
- Let forms/components handle business and validation-specific errors.
- Preserve the original
HttpErrorResponsewhen downstream code needs its status/body.
Interview Questions and Answers
1. What is ProblemDetails?
ProblemDetails is a standardized format for HTTP API errors. It commonly contains:
type
title
status
detail
instance
ASP.NET Core provides built-in support for producing Problem Details responses.
2. How does Angular consume ASP.NET Core ProblemDetails?
Angular receives it inside the HttpErrorResponse.error property.
const problem =
error.error as ProblemDetails;
console.log(problem.title);
console.log(problem.detail);
3. What is the difference between HttpErrorResponse and ProblemDetails?
HttpErrorResponse is Angular's representation of the failed HTTP operation.
ProblemDetails is the response body sent by the ASP.NET Core API.
HttpErrorResponse
|
+-- status
+-- url
+-- message
|
+-- error
|
+-- ProblemDetails
4. What is ValidationProblemDetails?
It is the validation-oriented Problem Details shape used by ASP.NET Core, adding field-level validation errors.
{
"status": 400,
"errors": {
"Name": [
"Name is required."
]
}
}
Angular can map those messages to Reactive Form controls.
5. Should Angular use detail to identify the type of error?
No. detail is human-readable text and may change.
Prefer a stable:
problem.type
or application-specific code such as:
PRODUCT_NOT_FOUND
for programmatic decisions.
6. Where should ProblemDetails be handled?
Common cross-cutting errors can be processed by an HTTP interceptor, while validation and business-specific problems should usually be handled by the relevant component or service.
7. Why is ProblemDetails useful?
It provides a consistent error contract between Angular and ASP.NET Core, reducing custom error formats and making client-side error handling easier and more predictable.
Interview Quick Answer
How should Angular consume ASP.NET Core
ProblemDetailsresponses?
Angular receives ASP.NET Core ProblemDetails in HttpErrorResponse.error. I define TypeScript interfaces for ProblemDetails and ValidationProblemDetails, use a global interceptor for common errors such as 401, 403, and 500, and let components handle business or validation errors. For programmatic decisions, I prefer a stable type or application error code rather than comparing human-readable error messages.