← Back to Articles         
HTTP Error Handling and Reusable API Service in Angular

HTTP Error Handling and Reusable API Service in Angular

Published on 01 Oct 2026     11 min read Angular
HTTP & Web API Integration

HTTP Error Handling and Reusable API Service in Angular

These two concepts are closely related. In a real Angular application, instead of repeating HttpClient, error handling, base URLs, and headers in every feature service, you can centralize common HTTP functionality.

Component
    ↓
ProductService
    ↓
Reusable ApiService
    ↓
HttpClient
    ↓
ASP.NET Core Web API

Full Simple Sample Program

This example uses:

  • HttpClient
  • HttpErrorResponse
  • catchError
  • throwError
  • Generic reusable ApiService
  • ProductService
  • ASP.NET Core Web API

app.config.ts

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient()
  ]
};

api.service.ts

This is our reusable generic API service.

import { inject, Injectable } from '@angular/core';
import {
  HttpClient,
  HttpErrorResponse
} from '@angular/common/http';

import {
  catchError,
  Observable,
  throwError
} from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class ApiService {

  private http = inject(HttpClient);

  private baseUrl =
    'https://localhost:7001/api';


  get<T>(endpoint: string): Observable<T> {

    return this.http
      .get<T>(`${this.baseUrl}/${endpoint}`)
      .pipe(
        catchError(error =>
          this.handleError(error)
        )
      );
  }


  post<T>(
    endpoint: string,
    data: unknown
  ): Observable<T> {

    return this.http
      .post<T>(
        `${this.baseUrl}/${endpoint}`,
        data
      )
      .pipe(
        catchError(error =>
          this.handleError(error)
        )
      );
  }


  put<T>(
    endpoint: string,
    data: unknown
  ): Observable<T> {

    return this.http
      .put<T>(
        `${this.baseUrl}/${endpoint}`,
        data
      )
      .pipe(
        catchError(error =>
          this.handleError(error)
        )
      );
  }


  delete<T>(
    endpoint: string
  ): Observable<T> {

    return this.http
      .delete<T>(
        `${this.baseUrl}/${endpoint}`
      )
      .pipe(
        catchError(error =>
          this.handleError(error)
        )
      );
  }


  private handleError(
    error: HttpErrorResponse
  ): Observable<never> {

    let message =
      'An unexpected error occurred.';

    if (error.status === 0) {

      message =
        'Unable to connect to the server.';

    }
    else if (error.status === 400) {

      message =
        'Invalid request.';

    }
    else if (error.status === 401) {

      message =
        'You are not authenticated.';

    }
    else if (error.status === 403) {

      message =
        'You are not authorized.';

    }
    else if (error.status === 404) {

      message =
        'Resource not found.';

    }
    else if (error.status >= 500) {

      message =
        'Server error occurred.';

    }

    console.error(
      'API Error:',
      error
    );

    return throwError(
      () => new Error(message)
    );
  }
}

product.ts

export interface Product {
  id: number;
  name: string;
  price: number;
}

product.service.ts

Notice that ProductService no longer directly uses HttpClient.

It uses our reusable ApiService.

import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';

import { ApiService } from './api.service';
import { Product } from './product';

@Injectable({
  providedIn: 'root'
})
export class ProductService {

  private api = inject(ApiService);


  getProducts(): Observable<Product[]> {

    return this.api.get<Product[]>(
      'products'
    );
  }


  getProduct(
    id: number
  ): Observable<Product> {

    return this.api.get<Product>(
      `products/${id}`
    );
  }


  addProduct(
    product: Product
  ): Observable<Product> {

    return this.api.post<Product>(
      'products',
      product
    );
  }


  updateProduct(
    id: number,
    product: Product
  ): Observable<void> {

    return this.api.put<void>(
      `products/${id}`,
      product
    );
  }


  deleteProduct(
    id: number
  ): Observable<void> {

    return this.api.delete<void>(
      `products/${id}`
    );
  }
}

products.ts

import {
  Component,
  inject,
  OnInit
} from '@angular/core';

import { ProductService } from './product.service';
import { Product } from './product';

@Component({
  selector: 'app-products',
  standalone: true,
  templateUrl: './products.html'
})
export class Products implements OnInit {

  private productService =
    inject(ProductService);

  products: Product[] = [];

  errorMessage = '';


  ngOnInit(): void {

    this.loadProducts();

  }


  loadProducts(): void {

    this.productService
      .getProducts()
      .subscribe({

        next: (data) => {

          this.products = data;

        },

        error: (error: Error) => {

          this.errorMessage =
            error.message;

        }

      });

  }
}

products.html

<h2>Products</h2>

@if (errorMessage) {
  <p>
    {{ errorMessage }}
  </p>
}

@for (product of products; track product.id) {

  <div>
    {{ product.name }}
    -
    ₹{{ product.price }}
  </div>

}

1. How Do You Handle HTTP Errors?

When Angular makes an API request, several things can go wrong.

For example:

Angular
   ↓
GET /api/products
   ↓
ASP.NET Core

Possible response:

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

Angular represents HTTP failures using:

HttpErrorResponse

Import:

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

Using catchError()

RxJS provides the:

catchError()

operator.

Example:

return this.http
  .get<Product[]>(url)
  .pipe(
    catchError(error =>
      this.handleError(error)
    )
  );

The flow is:

HttpClient.get()
      ↓
Observable
      ↓
HTTP Request
      ↓
       ┌───────────────┐
       │               │
    Success           Error
       │               │
       ↓               ↓
    next()         catchError()
                       ↓
                  handleError()

What is pipe()?

You will frequently see:

this.http
  .get<Product[]>(url)
  .pipe(
    catchError(...)
  );

pipe() allows RxJS operators to process the Observable.

Conceptually:

Observable
    ↓
pipe()
    ↓
RxJS Operators
    ↓
Modified Observable

Examples of RxJS operators include:

catchError()
map()
tap()
retry()
switchMap()

For error handling:

.pipe(
  catchError(...)
)

is commonly used.


Understanding HttpErrorResponse

Suppose the ASP.NET Core API returns:

404 Not Found

Angular receives an HttpErrorResponse.

You can inspect:

error.status
error.statusText
error.message
error.error
error.url

For example:

private handleError(
  error: HttpErrorResponse
) {

  console.log(error.status);
  console.log(error.message);
  console.log(error.error);

}

Common HTTP Status Codes

Status Meaning
0 Network/CORS/connectivity-type failure from Angular's perspective
400 Bad Request
401 Unauthenticated / invalid authentication
403 Authenticated but forbidden
404 Resource not found
409 Conflict
422 Unprocessable content / validation semantics in APIs that use it
500 Internal Server Error
503 Service Unavailable

For example:

if (error.status === 404) {

  message =
    'Resource not found.';

}

status === 0

This is important.

Angular can report:

error.status === 0

when no normal HTTP response was available to the application.

Possible causes include:

  • Backend unavailable
  • Network problem
  • CORS failure
  • DNS/connectivity problem
  • TLS/certificate-related failure

So don't interpret 0 as an HTTP status code returned by ASP.NET Core.


What Does throwError() Do?

After processing the error:

return throwError(
  () => new Error(message)
);

we return an error Observable.

That allows the component's error callback to execute:

.subscribe({

  next: data => {
    // Success
  },

  error: error => {
    // Failure
  }

});

Flow:

API Error
   ↓
catchError()
   ↓
handleError()
   ↓
throwError()
   ↓
Component
   ↓
subscribe.error()

Error Handling at Component Level

The component can handle the user-facing behavior:

this.productService
  .getProducts()
  .subscribe({

    next: data => {

      this.products = data;

    },

    error: error => {

      this.errorMessage =
        error.message;

    }

  });

This is useful because the component decides what the UI should do.

For example:

404
 ↓
Display "Product not found"

500
 ↓
Display "Please try again later"

401
 ↓
Authentication workflow

Global Error Handling with an HTTP Interceptor

For large applications, some HTTP errors should be handled centrally.

For example:

401 → authentication handling
403 → authorization handling
500 → common error logging/message

An HTTP interceptor is often better for these cross-cutting concerns.

Component
    ↓
Service
    ↓
HttpClient
    ↓
HTTP Interceptor
    ↓
ASP.NET Core

The interceptor can inspect responses from many API calls.

A simple example:

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 === 401) {

          console.log(
            'Authentication required'
          );

        }

        if (error.status >= 500) {

          console.log(
            'Server error'
          );

        }

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

  );
};

Configure it:

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

providers: [

  provideHttpClient(
    withInterceptors([
      errorInterceptor
    ])
  )

]

Where Should Errors Be Handled?

A good architecture separates responsibilities.

HTTP Interceptor
       ↓
Cross-cutting HTTP concerns

Reusable ApiService
       ↓
Common API mechanics

Feature Service
       ↓
Business/API-specific operations

Component
       ↓
UI-specific handling

For example:

Error concern Good location
Log common HTTP failures Interceptor
Handle authentication failure Authentication/interceptor layer
Convert common API errors API service
Show validation message Component/form
Show "Product not found" Product UI
Retry selected transient requests Appropriate service/interceptor policy

You don't have to put all error handling in one place.


2. How Do You Create a Reusable API Service?

Without a reusable service, you may end up writing:

this.http.get(...)

in:

ProductService
CategoryService
ArticleService
UserService
OrderService
CustomerService

and repeatedly write:

Base URL
Error handling
Headers
GET
POST
PUT
DELETE

A reusable API service centralizes common HTTP mechanics.


Generic ApiService

The important part is:

get<T>()
post<T>()
put<T>()
delete<T>()

For example:

get<T>(
  endpoint: string
): Observable<T> {

  return this.http.get<T>(
    `${this.baseUrl}/${endpoint}`
  );

}

T makes the service generic.


What Does <T> Mean?

Suppose we call:

this.api.get<Product[]>(
  'products'
);

Here:

T = Product[]

Therefore:

Observable<T>

becomes:

Observable<Product[]>

If we call:

this.api.get<Category[]>(
  'categories'
);

then:

T = Category[]

and the return type becomes:

Observable<Category[]>

That's what makes the API service reusable.


Example: Product Service

getProducts(): Observable<Product[]> {

  return this.api.get<Product[]>(
    'products'
  );

}

Resulting URL:

https://localhost:7001/api/products

Example: Category Service

The same ApiService can be reused:

@Injectable({
  providedIn: 'root'
})
export class CategoryService {

  private api = inject(ApiService);

  getCategories(): Observable<Category[]> {

    return this.api.get<Category[]>(
      'categories'
    );

  }
}

Now:

ProductService
       │
       ├─────┐
             ↓
          ApiService
             ↑
       ┌─────┘
       │
CategoryService

Both use the same HTTP infrastructure.


Where Should the Base URL Be Stored?

For a very simple learning example we used:

private baseUrl =
  'https://localhost:7001/api';

In a real application, don't normally hard-code environment-specific URLs throughout your services.

For example:

Development
https://localhost:7001/api

UAT
https://uat-api.example.com/api

Production
https://api.example.com/api

The API base URL should come from your application's environment/configuration strategy.

Then the reusable service uses that configured value.


Reusable API Service Architecture

A clean Angular structure can look like:

Angular Component
        ↓
Feature Service
(ProductService)
        ↓
Generic ApiService
        ↓
HttpClient
        ↓
HTTP Interceptors
        ↓
ASP.NET Core Web API
        ↓
Application Layer
        ↓
Infrastructure
        ↓
SQL Server

Responsibilities:

Component

UI behavior
Display data
Display errors
User interaction

ProductService

Product-specific API operations

For example:

getProducts()
addProduct()
deleteProduct()

ApiService

Common HTTP functionality
GET
POST
PUT
DELETE
Base URL
Common error transformation

Interceptor

Cross-cutting HTTP behavior
JWT
Logging
Common error handling
Correlation ID

This distinction is useful in larger Angular applications.


Should Components Call ApiService Directly?

Technically, they can.

For example:

this.api.get<Product[]>(
  'products'
);

But for a structured application, I generally prefer:

Products Component
        ↓
ProductService
        ↓
ApiService
        ↓
HttpClient

rather than:

Products Component
        ↓
ApiService

because ProductService gives you a clear feature/domain API:

getProducts()
getProduct(id)
createProduct(product)
updateProduct(id, product)
deleteProduct(id)

instead of spreading endpoint strings such as:

'products'
'products/10'

through UI components.


ApiService vs ProductService

This distinction is important.

ApiService

Generic:

get<T>()
post<T>()
put<T>()
delete<T>()

It doesn't need to know what a product is.


ProductService

Product-specific:

getProducts()
getProduct()
addProduct()
updateProduct()
deleteProduct()

Internally:

return this.api.get<Product[]>(
  'products'
);

Therefore:

ProductService
    ↓
"What operation do I want?"

ApiService
    ↓
"How do I communicate over HTTP?"

HttpClient
    ↓
"Send the HTTP request."

Key Points

  • Angular represents HTTP failures with HttpErrorResponse.
  • Use RxJS catchError() to intercept errors in an Observable pipeline.
  • Use throwError() when you want to propagate an error downstream.
  • error.status contains the HTTP status when an HTTP response exists.
  • Status 0 usually indicates that Angular didn't receive a normal HTTP response.
  • Use components for UI-specific error behavior.
  • Use interceptors for cross-cutting HTTP concerns.
  • A reusable ApiService can centralize:
    • Base URL handling
    • GET
    • POST
    • PUT
    • DELETE
    • Common error transformation
  • Generics such as <T> allow one API service to work with different response models.
  • Feature services such as ProductService should contain feature-specific API operations.
  • Keep environment-specific API URLs out of individual feature services.

Interview Questions and Answers

1. How do you handle HTTP errors in Angular?

Use RxJS catchError() with Angular's HttpErrorResponse.

return this.http
  .get<Product[]>(url)
  .pipe(
    catchError(error =>
      this.handleError(error)
    )
  );

2. What is HttpErrorResponse?

HttpErrorResponse represents an error associated with an Angular HTTP request and provides information such as:

error.status
error.message
error.error
error.url

3. What is catchError()?

catchError() is an RxJS operator used to catch and process errors emitted by an Observable.

.pipe(
  catchError(error => ...)
)

4. What does throwError() do?

It creates an Observable that emits an error, allowing the failure to propagate to downstream subscribers.

return throwError(
  () => new Error(message)
);

5. What does HTTP status 0 mean in Angular?

It means Angular didn't receive a normal HTTP response. Possible causes include network connectivity, CORS, server availability, DNS, or TLS-related problems.


6. What is a reusable API service?

It is a shared service that centralizes common HTTP functionality such as:

get<T>()
post<T>()
put<T>()
delete<T>()

so feature services don't repeatedly implement the same low-level HTTP code.


7. Why use generics in an API service?

Generics allow the same method to return different strongly typed responses.

api.get<Product[]>('products');

api.get<Category[]>('categories');

The same get<T>() implementation supports both.


8. What is the difference between ApiService and ProductService?

ApiService contains generic HTTP functionality, while ProductService contains product-specific API operations.

ProductService
     ↓
ApiService
     ↓
HttpClient

9. Should all HTTP errors be handled in components?

No. Cross-cutting concerns can be handled centrally with an HTTP interceptor, while components should handle errors that affect their specific UI.


10. What is the recommended flow for a structured Angular application?

Component
    ↓
Feature Service
    ↓
Reusable ApiService
    ↓
HttpClient
    ↓
Interceptor
    ↓
ASP.NET Core Web API

Short Interview Answer

Angular HTTP errors can be handled using HttpErrorResponse and the RxJS catchError() operator, with interceptors used for cross-cutting error handling. A reusable API service wraps HttpClient with generic methods such as get<T>(), post<T>(), put<T>(), and delete<T>(), while feature services such as ProductService expose business-specific API operations.

← Back to Articles