Skip to main content
Error Handling

Error Handling Overview

Estimated Time: 2 hours | Difficulty: Intermediate | Prerequisites: Services, HTTP Client, RxJS
Robust error handling ensures your application gracefully handles failures, provides meaningful feedback to users, and helps developers debug issues effectively.
┌─────────────────────────────────────────────────────────────────────────┐
│                    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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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