Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

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. The brutal truth is that most Angular apps handle errors poorly — they either swallow them silently (the user sees a blank screen and has no idea what happened) or show raw technical messages (“HttpErrorResponse: 500 Internal Server Error” means nothing to an end user). Good error handling is like a building’s fire safety system: it operates at multiple levels (smoke detectors in every room, sprinklers per floor, fire escapes for the whole building), and each level has a clear responsibility. In Angular terms, that means component-level error states for recoverable issues, an HTTP interceptor for API failures, and a global ErrorHandler as the last line of defense.
┌─────────────────────────────────────────────────────────────────────────┐
│                    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 server errors and network timeouts with linear backoff.
    // We retry up to 2 times (3 total attempts), waiting retryCount * 1000ms
    // between attempts. This handles transient failures (brief network blips,
    // momentary server overload) without hammering a truly-down server.
    retry({
      count: 2,
      delay: (error, retryCount) => {
        if (shouldRetry(error)) {
          return timer(retryCount * 1000);  // 1s, then 2s backoff
        }
        // Don't retry client errors -- the request itself is wrong
        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 most client errors (4xx) -- the request itself is wrong.
  // Exceptions: 408 (Request Timeout) and 429 (Too Many Requests) are
  // transient and worth retrying after a delay.
  if (error.status >= 400 && error.status < 500) {
    return [408, 429].includes(error.status);
  }
  // Retry server errors (5xx) and status 0 (network failure / CORS error).
  // Status 0 is particularly tricky -- it means the request never reached
  // the server, which can indicate a network blip OR a CORS misconfiguration.
  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 boundaries isolate failures to a specific section of the UI rather than crashing the entire application. Think of them like circuit breakers in electrical systems — when one circuit shorts, it trips its own breaker instead of blowing the main fuse and killing power to the whole house. In Angular, you implement this by providing a local ErrorHandler at the component level, so errors in child components are caught by the boundary rather than propagating to the global handler.
When to use error boundaries: Wrap any section of your UI that loads third-party content, renders user-generated HTML, or depends on unreliable data sources. Dashboard widgets, embedded iframes, and plugin-rendered content are all excellent candidates.
// 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