← Back to Articles         
How should Angular consume ASP.NET Core ProblemDetails responses?

How should Angular consume ASP.NET Core ProblemDetails responses?

Published on 02 Oct 2026     10 min read Angular
Error Handling

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

  • ProblemDetails provides 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 ProblemDetails interface rather than repeatedly using untyped properties.
  • ASP.NET Core validation failures commonly use ValidationProblemDetails.
  • Model ValidationProblemDetails separately with an errors dictionary.
  • title is a short summary.
  • detail describes the specific occurrence.
  • status contains the HTTP status represented by the problem response.
  • type can provide a stable machine-readable problem category.
  • instance identifies the particular occurrence/resource.
  • Custom extensions such as traceId or errorCode can be useful.
  • Don't write business logic that depends on matching human-readable detail text.
  • Let the interceptor handle global HTTP concerns.
  • Let forms/components handle business and validation-specific errors.
  • Preserve the original HttpErrorResponse when 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 ProblemDetails responses?

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.

← Back to Articles