← Back to Articles         
How would you globally handle API errors?

How would you globally handle API errors?

Published on 02 Oct 2026     7 min read Angular
Scenario Based Questions

For an Angular + ASP.NET Core Web API application, these are usually handled at two levels:

  1. Global/API-level errors → Angular HTTP interceptor.
  2. Form validation errors → ASP.NET Core returns ValidationProblemDetails, and Angular maps the errors object to form controls.

1. How would you globally handle API errors?

What is global API error handling?

Instead of writing catchError() in every Angular service:

this.http.get(...).pipe(
  catchError(...)
);

we can create a global HTTP error interceptor.

It can centrally handle errors such as:

400 → Bad Request
401 → Unauthorized
403 → Forbidden
404 → Not Found
500 → Internal Server Error

The flow is:

Component
    ↓
Service
    ↓
HttpClient
    ↓
Error Interceptor
    ↓
ASP.NET Core API
    ↓
HTTP Error
    ↓
Error Interceptor
    ↓
Global handling

Simple Functional Error Interceptor

error-interceptor.ts

import { inject } from '@angular/core';

import {
  HttpErrorResponse,
  HttpInterceptorFn
} from '@angular/common/http';

import {
  catchError,
  throwError
} from 'rxjs';

import { Router } from '@angular/router';

export const errorInterceptor: HttpInterceptorFn =
  (req, next) => {

    const router = inject(Router);

    return next(req).pipe(

      catchError((error: HttpErrorResponse) => {

        switch (error.status) {

          case 400:
            console.error(
              'Bad Request:',
              error.error
            );
            break;

          case 401:
            console.error('Unauthorized');

            router.navigate(['/login']);
            break;

          case 403:
            console.error('Forbidden');

            router.navigate(['/forbidden']);
            break;

          case 404:
            console.error('Resource not found');
            break;

          case 500:
            console.error(
              'Server error:',
              error.error
            );

            router.navigate(['/server-error']);
            break;

          default:
            console.error(
              'Unexpected API error:',
              error
            );
        }

        return throwError(() => error);
      })
    );
  };

Register globally:

app.config.ts

import {
  ApplicationConfig
} from '@angular/core';

import {
  provideHttpClient,
  withInterceptors
} from '@angular/common/http';

import {
  jwtInterceptor
} from './interceptors/jwt-interceptor';

import {
  errorInterceptor
} from './interceptors/error-interceptor';

export const appConfig: ApplicationConfig = {

  providers: [

    provideHttpClient(
      withInterceptors([
        jwtInterceptor,
        errorInterceptor
      ])
    )

  ]
};

Now every request made through HttpClient goes through these interceptors.


Handling Different Status Codes

400 — Bad Request

Usually represents invalid request data or validation problems.

case 400:

  console.error(
    'Bad request',
    error.error
  );

  break;

For validation errors, however, we usually let the component access the actual validation response.


401 — Unauthorized

Example:

case 401:

  router.navigate(['/login']);

  break;

However, if your application supports refresh tokens, don't immediately redirect every 401 to login.

The authentication interceptor should first attempt:

401
 ↓
Try refresh token
 ↓
Refresh successful?
 ├── Yes → retry request
 │
 └── No → logout/login

403 — Forbidden

The user is authenticated but doesn't have sufficient authorization.

case 403:

  router.navigate(['/forbidden']);

  break;

For example:

User has valid JWT
       ↓
Role = User
       ↓
Calls Admin API
       ↓
403 Forbidden

404 — Not Found

case 404:

  console.error(
    'Requested resource was not found'
  );

  break;

500 — Internal Server Error

case 500:

  console.error(
    'Internal server error'
  );

  router.navigate(['/server-error']);

  break;

The UI should normally display a user-friendly message rather than exposing exception details or stack traces.


ASP.NET Core Side

Angular's global handling becomes much cleaner when ASP.NET Core returns a consistent error format.

ASP.NET Core commonly uses ProblemDetails.

Example:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
  "title": "An unexpected error occurred.",
  "status": 500,
  "detail": "Unable to process the request."
}

Angular can define:

export interface ApiProblem {
  type?: string;
  title?: string;
  status?: number;
  detail?: string;
}

Then:

const problem =
  error.error as ApiProblem;

console.log(problem.title);
console.log(problem.detail);

2. How would you display ASP.NET Core validation errors in Angular?

This is slightly different from general API errors.

Suppose we have an ASP.NET Core DTO.

ASP.NET Core DTO

public class CreateProductDto
{
    [Required]
    public string Name { get; set; } = string.Empty;

    [Range(1, 100000)]
    public decimal Price { get; set; }
}

Controller:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpPost]
    public IActionResult Create(
        CreateProductDto product)
    {
        return Ok();
    }
}

Because [ApiController] is used, ASP.NET Core automatically performs model validation.


Example Invalid Angular Request

Angular sends:

{
  "name": "",
  "price": 0
}

ASP.NET Core can return a 400 Bad Request containing validation errors similar to:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Name": [
      "The Name field is required."
    ],
    "Price": [
      "The field Price must be between 1 and 100000."
    ]
  }
}

The most important part for Angular is:

"errors": {
  "Name": [
    "The Name field is required."
  ],
  "Price": [
    "The field Price must be between 1 and 100000."
  ]
}

Angular Validation Error Interface

Create an interface corresponding to ASP.NET Core's validation response.

export interface ValidationProblem {
  type?: string;
  title?: string;
  status?: number;

  errors: {
    [key: string]: string[];
  };
}

The index signature:

[key: string]: string[];

means:

Name  → string[]
Price → string[]
Email → string[]
etc.

Angular Reactive Form

Suppose we have:

productForm = new FormGroup({

  name: new FormControl('', {
    nonNullable: true
  }),

  price: new FormControl(0, {
    nonNullable: true
  })

});

Submit:

save(): void {

  this.productService
    .create(this.productForm.getRawValue())
    .subscribe({

      next: response => {

        console.log('Product created');

      },

      error: error => {

        if (error.status === 400) {

          this.applyServerValidationErrors(
            error.error.errors
          );

        }

      }

    });
}

Map ASP.NET Core Errors to Angular Controls

Create:

applyServerValidationErrors(
  errors: Record<string, string[]>
): void {

  Object.keys(errors).forEach(serverField => {

    const controlName =
      serverField.charAt(0).toLowerCase()
      + serverField.slice(1);

    const control =
      this.productForm.get(controlName);

    if (control) {

      control.setErrors({
        ...control.errors,

        server: errors[serverField]
      });

      control.markAsTouched();
    }

  });
}

ASP.NET Core returns:

Name
Price

while your Angular form controls may be:

name
price

Therefore this example converts:

Name  → name
Price → price

In larger applications, it's better to standardize field naming between the API contract and Angular rather than rely on ad-hoc case conversion.


Display Errors in HTML

product.html

<form [formGroup]="productForm"
      (ngSubmit)="save()">

  <div>

    <label>Name</label>

    <input
      type="text"
      formControlName="name">

    @if (productForm.controls.name.errors?.['server']) {

      @for (
        error of productForm.controls.name.errors?.['server'];
        track error
      ) {

        <div class="error">
          {{ error }}
        </div>

      }

    }

  </div>


  <div>

    <label>Price</label>

    <input
      type="number"
      formControlName="price">

    @if (productForm.controls.price.errors?.['server']) {

      @for (
        error of productForm.controls.price.errors?.['server'];
        track error
      ) {

        <div class="error">
          {{ error }}
        </div>

      }

    }

  </div>

  <button type="submit">
    Save
  </button>

</form>

Now if ASP.NET Core returns:

{
  "errors": {
    "Name": [
      "The Name field is required."
    ]
  }
}

Angular displays:

Name
[                     ]

The Name field is required.

Client-Side + Server-Side Validation

In a real application, you normally use both.

Angular:

name: new FormControl('', [
  Validators.required
])

provides immediate feedback.

ASP.NET Core:

[Required]
public string Name { get; set; }

provides authoritative server-side validation.

The flow becomes:

                 Angular Form
                      │
             Client Validation
                      │
                Valid locally
                      ↓
               POST /products
                      │
                      ↓
              ASP.NET Core
                      │
             Server Validation
                /           \
             Valid          Invalid
               │               │
             200 OK          400
                               │
                    ValidationProblemDetails
                               │
                               ↓
                         Angular Form
                               │
                      setErrors(...)
                               │
                               ↓
                     Display field errors

Angular validation improves UX. ASP.NET Core validation protects the API.

Never depend only on Angular validation because a client can bypass Angular completely and call the API directly.


Should the Global Interceptor Display Validation Errors?

Usually, not field-level validation errors.

The interceptor can handle general infrastructure/authentication errors:

401
403
404
500

But a component knows which form control corresponds to:

Name
Email
Price

So a useful separation is:

                 API Response
                      │
                HTTP Error
                      │
            ┌─────────┴─────────┐
            │                   │
       General Error       Validation Error
            │                   │
      Interceptor           Component/Form
            │                   │
        401/403/500        Name/Price/Email
            │                   │
      Global handling      setErrors(...)

You can still use a shared validation utility/service so every component doesn't duplicate the mapping logic.


Key Points

  • Use an HTTP interceptor for global API error handling.
  • Use catchError() to inspect HttpErrorResponse.
  • Handle 401, 403, 404, 500, etc. consistently.
  • Coordinate 401 handling with your refresh-token interceptor.
  • ASP.NET Core should preferably return standardized ProblemDetails / ValidationProblemDetails.
  • [ApiController] automatically produces 400 responses for invalid model state in common controller scenarios.
  • ASP.NET Core validation errors are commonly returned through an errors dictionary.
  • Angular can map those server errors to reactive form controls using setErrors().
  • Client-side validation is for fast UX; server-side validation remains authoritative.

Interview Answers

Q1. How would you globally handle API errors in Angular?

I would create an HTTP interceptor and use catchError() to centrally process HttpErrorResponse. Based on the status code, I can handle 401, 403, 404, 500, logging, navigation, and user-friendly messages consistently across the application.

Q2. How would you display ASP.NET Core validation errors in Angular?

ASP.NET Core can return validation failures as ValidationProblemDetails, including an errors dictionary. Angular reads that dictionary, maps each server field to the corresponding reactive form control, calls setErrors(), and displays those messages next to the relevant fields.

← Back to Articles