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:
HttpClientHttpErrorResponsecatchErrorthrowError- 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.statuscontains the HTTP status when an HTTP response exists.- Status
0usually 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
ApiServicecan 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
ProductServiceshould 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
HttpErrorResponseand the RxJScatchError()operator, with interceptors used for cross-cutting error handling. A reusable API service wrapsHttpClientwith generic methods such asget<T>(),post<T>(),put<T>(), anddelete<T>(), while feature services such asProductServiceexpose business-specific API operations.