Error Handling Overview
Estimated Time: 2 hours | Difficulty: Intermediate | Prerequisites: Services, HTTP Client, RxJS
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ Error Handling Strategy │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ HTTP │ │ Component │ │ Runtime │ │
│ │ Errors │ │ Errors │ │ Errors │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Error Interceptor / Handler │ │
│ └───────────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Notify │ │ Log │ │ Recover │ │
│ │ User │ │ Error │ │ Graceful │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Toast │ │ Sentry/ │ │ Fallback │ │
│ │ Error │ │ Analytics │ │ UI/Data │ │
│ │ Modal │ │ Console │ │ Retry │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Global Error Handler
Copy
// error-handler.service.ts
import { ErrorHandler, Injectable, inject, NgZone } from '@angular/core';
import { Router } from '@angular/router';
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
private zone = inject(NgZone);
private router = inject(Router);
private errorService = inject(ErrorService);
private notificationService = inject(NotificationService);
handleError(error: Error | HttpErrorResponse): void {
// Log error
this.errorService.logError(error);
// Run inside Angular zone for UI updates
this.zone.run(() => {
if (error instanceof HttpErrorResponse) {
this.handleHttpError(error);
} else {
this.handleRuntimeError(error);
}
});
}
private handleHttpError(error: HttpErrorResponse): void {
switch (error.status) {
case 401:
this.router.navigate(['/login']);
break;
case 403:
this.notificationService.error('Access denied');
break;
case 404:
this.router.navigate(['/not-found']);
break;
case 500:
this.notificationService.error('Server error. Please try again.');
break;
default:
this.notificationService.error('An error occurred');
}
}
private handleRuntimeError(error: Error): void {
// Check for specific error types
if (error.name === 'ChunkLoadError') {
this.notificationService.warning(
'New version available. Refreshing...'
);
window.location.reload();
return;
}
this.notificationService.error('An unexpected error occurred');
// Navigate to error page for critical errors
if (this.isCriticalError(error)) {
this.router.navigate(['/error']);
}
}
private isCriticalError(error: Error): boolean {
return error.message.includes('Cannot read') ||
error.message.includes('is not defined');
}
}
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler }
]
};
HTTP Error Interceptor
Copy
// error.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError, retry, timer } from 'rxjs';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const errorService = inject(ErrorService);
const authService = inject(AuthService);
return next(req).pipe(
// Retry failed requests (except auth)
retry({
count: 2,
delay: (error, retryCount) => {
if (shouldRetry(error)) {
return timer(retryCount * 1000);
}
return throwError(() => error);
}
}),
catchError((error: HttpErrorResponse) => {
// Transform error for consistent handling
const appError = errorService.createAppError(error);
// Handle token expiration
if (error.status === 401 && !req.url.includes('/auth/')) {
authService.refreshToken().subscribe({
error: () => authService.logout()
});
}
return throwError(() => appError);
})
);
};
function shouldRetry(error: HttpErrorResponse): boolean {
// Don't retry client errors (4xx) except 408, 429
if (error.status >= 400 && error.status < 500) {
return [408, 429].includes(error.status);
}
// Retry server errors (5xx) and network errors
return error.status >= 500 || error.status === 0;
}
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([errorInterceptor])
)
]
};
Error Service
Copy
// error.service.ts
@Injectable({ providedIn: 'root' })
export class ErrorService {
private http = inject(HttpClient);
createAppError(error: HttpErrorResponse): AppError {
return {
message: this.getErrorMessage(error),
code: error.status.toString(),
timestamp: new Date().toISOString(),
url: error.url ?? 'unknown',
details: error.error
};
}
logError(error: Error | HttpErrorResponse): void {
const errorLog: ErrorLog = {
message: error.message,
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
};
// Log to console in development
if (!environment.production) {
console.error('Error:', errorLog);
}
// Send to error tracking service
this.sendToErrorTracking(errorLog);
}
private getErrorMessage(error: HttpErrorResponse): string {
// Server-provided message
if (error.error?.message) {
return error.error.message;
}
// Standard HTTP messages
const messages: Record<number, string> = {
0: 'Unable to connect to server',
400: 'Invalid request',
401: 'Please log in to continue',
403: 'You do not have permission',
404: 'Resource not found',
408: 'Request timed out',
429: 'Too many requests. Please wait.',
500: 'Server error. Please try again.',
502: 'Service temporarily unavailable',
503: 'Service unavailable',
504: 'Gateway timeout'
};
return messages[error.status] ?? 'An unexpected error occurred';
}
private sendToErrorTracking(log: ErrorLog): void {
// Integrate with Sentry, LogRocket, etc.
this.http.post('/api/logs/errors', log).subscribe({
error: () => console.warn('Failed to send error log')
});
}
}
interface AppError {
message: string;
code: string;
timestamp: string;
url: string;
details?: unknown;
}
interface ErrorLog {
message: string;
stack?: string;
timestamp: string;
userAgent: string;
url: string;
}
Component-Level Error Handling
Copy
// data.component.ts
@Component({
selector: 'app-data',
template: `
@if (loading()) {
<app-skeleton-loader />
} @else if (error()) {
<app-error-state
[error]="error()!"
[retryable]="true"
(retry)="loadData()"
/>
} @else {
@for (item of data(); track item.id) {
<app-item-card [item]="item" />
}
}
`
})
export class DataComponent {
private dataService = inject(DataService);
data = signal<Item[]>([]);
loading = signal(true);
error = signal<AppError | null>(null);
ngOnInit() {
this.loadData();
}
loadData() {
this.loading.set(true);
this.error.set(null);
this.dataService.getItems()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (items) => {
this.data.set(items);
this.loading.set(false);
},
error: (error: AppError) => {
this.error.set(error);
this.loading.set(false);
}
});
}
}
// error-state.component.ts
@Component({
selector: 'app-error-state',
standalone: true,
template: `
<div class="error-state">
<div class="error-icon">⚠️</div>
<h3>{{ error().message }}</h3>
@if (error().code) {
<p class="error-code">Error code: {{ error().code }}</p>
}
@if (retryable()) {
<button (click)="retry.emit()">
Try Again
</button>
}
</div>
`,
styleUrl: './error-state.component.scss'
})
export class ErrorStateComponent {
error = input.required<AppError>();
retryable = input(false);
retry = output<void>();
}
Error Boundaries
Copy
// error-boundary.component.ts
@Component({
selector: 'app-error-boundary',
standalone: true,
template: `
@if (hasError()) {
<div class="error-boundary">
<h2>Something went wrong</h2>
<p>We're sorry, but something unexpected happened.</p>
<button (click)="reset()">Try Again</button>
</div>
} @else {
<ng-content />
}
`
})
export class ErrorBoundaryComponent implements ErrorHandler {
hasError = signal(false);
error = signal<Error | null>(null);
private zone = inject(NgZone);
handleError(error: Error): void {
this.zone.run(() => {
this.hasError.set(true);
this.error.set(error);
});
}
reset() {
this.hasError.set(false);
this.error.set(null);
}
}
// Usage with providers
@Component({
template: `
<app-error-boundary>
<app-risky-feature />
</app-error-boundary>
`,
providers: [
{
provide: ErrorHandler,
useExisting: ErrorBoundaryComponent
}
]
})
export class FeatureContainerComponent {}
RxJS Error Handling
Copy
// Advanced RxJS error patterns
@Injectable({ providedIn: 'root' })
export class DataService {
private http = inject(HttpClient);
// Retry with exponential backoff
getDataWithRetry(): Observable<Data> {
return this.http.get<Data>('/api/data').pipe(
retryWhen(errors =>
errors.pipe(
scan((retryCount, error) => {
if (retryCount >= 3) {
throw error;
}
return retryCount + 1;
}, 0),
delayWhen(retryCount =>
timer(Math.pow(2, retryCount) * 1000)
)
)
)
);
}
// Fallback to cached data
getDataWithFallback(): Observable<Data> {
return this.http.get<Data>('/api/data').pipe(
catchError(() => this.getCachedData())
);
}
// Partial failure handling
getMultipleItems(ids: string[]): Observable<Item[]> {
const requests = ids.map(id =>
this.http.get<Item>(`/api/items/${id}`).pipe(
catchError(() => of(null)) // Return null for failed items
)
);
return forkJoin(requests).pipe(
map(items => items.filter((item): item is Item => item !== null))
);
}
// Timeout with custom error
getDataWithTimeout(): Observable<Data> {
return this.http.get<Data>('/api/data').pipe(
timeout({
each: 10000,
with: () => throwError(() => ({
message: 'Request timed out',
code: 'TIMEOUT'
}))
})
);
}
}
Form Validation Errors
Copy
// form-error.component.ts
@Component({
selector: 'app-form-error',
standalone: true,
template: `
@if (control().invalid && (control().dirty || control().touched)) {
<div class="form-error" role="alert">
{{ errorMessage() }}
</div>
}
`
})
export class FormErrorComponent {
control = input.required<AbstractControl>();
private errorMessages: Record<string, (params: any) => string> = {
required: () => 'This field is required',
email: () => 'Please enter a valid email',
minlength: (p) => `Minimum ${p.requiredLength} characters required`,
maxlength: (p) => `Maximum ${p.requiredLength} characters allowed`,
min: (p) => `Value must be at least ${p.min}`,
max: (p) => `Value must be at most ${p.max}`,
pattern: () => 'Invalid format',
passwordMismatch: () => 'Passwords do not match',
uniqueEmail: () => 'This email is already registered'
};
errorMessage = computed(() => {
const errors = this.control().errors;
if (!errors) return '';
const firstError = Object.keys(errors)[0];
const errorFn = this.errorMessages[firstError];
return errorFn ? errorFn(errors[firstError]) : 'Invalid value';
});
}
Sentry Integration
Copy
// sentry.config.ts
import * as Sentry from '@sentry/angular';
Sentry.init({
dsn: environment.sentryDsn,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration()
],
tracesSampleRate: 1.0,
tracePropagationTargets: ['localhost', /^https:\/\/api\.myapp\.com/],
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0
});
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
{
provide: ErrorHandler,
useValue: Sentry.createErrorHandler({
showDialog: true
})
},
{
provide: Sentry.TraceService,
deps: [Router]
},
{
provide: APP_INITIALIZER,
useFactory: () => () => {},
deps: [Sentry.TraceService],
multi: true
}
]
};
// Custom error context
@Injectable({ providedIn: 'root' })
export class ErrorReporter {
reportError(error: Error, context?: Record<string, any>): void {
Sentry.withScope(scope => {
if (context) {
scope.setExtras(context);
}
Sentry.captureException(error);
});
}
setUser(user: User): void {
Sentry.setUser({
id: user.id,
email: user.email
});
}
}
Best Practices
Centralize Handling
Use global handler for consistent error processing
User-Friendly Messages
Never show technical errors to users
Log Everything
Capture context for debugging
Graceful Degradation
Provide fallbacks when possible
Next: Interview Questions
Prepare for Angular technical interviews