# Error Handling

> Error handling and reporting patterns for both frontend and backend in PastCare

- Skill: `majiayu000/error-handling-13` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add majiayu000/error-handling-13`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/error-handling-13/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/error-handling-13

---


# Error Handling Skill

Implement error handling for: $ARGUMENTS

## Backend Error Response Format

### Standard Error Response
```java
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidationErrors(
            MethodArgumentNotValidException ex) {
        Map<String, List<String>> fieldErrors = new HashMap<>();

        ex.getBindingResult().getFieldErrors().forEach(error -> {
            fieldErrors.computeIfAbsent(error.getField(), k -> new ArrayList<>())
                .add(error.getDefaultMessage());
        });

        Map<String, Object> response = new HashMap<>();
        response.put("status", 400);
        response.put("error", "Validation Failed");
        response.put("timestamp", Instant.now());
        response.putAll(fieldErrors);  // Field errors at root level

        return ResponseEntity.badRequest().body(response);
    }
}
```

### Field-Level Error Format
```json
{
  "status": 400,
  "error": "Validation Failed",
  "timestamp": "2024-01-15T10:30:00Z",
  "email": ["Email is required", "Invalid email format"],
  "phone": ["Phone number must be 10 digits"]
}
```

### Business Logic Errors
```java
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Map<String, Object>> handleBusinessError(BusinessException ex) {
    return ResponseEntity
        .status(ex.getStatus())
        .body(Map.of(
            "status", ex.getStatus().value(),
            "error", ex.getMessage(),
            "timestamp", Instant.now()
        ));
}
```

### Access Denied
```java
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<Map<String, Object>> handleAccessDenied(AccessDeniedException ex) {
    return ResponseEntity
        .status(HttpStatus.FORBIDDEN)
        .body(Map.of(
            "status", 403,
            "error", "Access denied",
            "timestamp", Instant.now()
        ));
}
```

## Frontend Error Handling

### HTTP Error Interceptor
```typescript
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  constructor(
    private messageService: MessageService,
    private router: Router
  ) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((error: HttpErrorResponse) => {
        switch (error.status) {
          case 401:
            this.router.navigate(['/login']);
            break;
          case 403:
            this.messageService.add({
              severity: 'error',
              summary: 'Access Denied',
              detail: 'You do not have permission to perform this action'
            });
            break;
          case 404:
            this.messageService.add({
              severity: 'error',
              summary: 'Not Found',
              detail: 'The requested resource was not found'
            });
            break;
          case 500:
            this.messageService.add({
              severity: 'error',
              summary: 'Server Error',
              detail: 'An unexpected error occurred. Please try again.'
            });
            break;
        }
        return throwError(() => error);
      })
    );
  }
}
```

### Component-Level Error Handling
```typescript
export class MemberFormComponent {
  errorMessage = signal<string | null>(null);
  backendFieldErrors = signal<Record<string, string[]>>({});
  isLoading = signal(false);

  saveMember(): void {
    this.clearErrors();
    this.isLoading.set(true);

    this.memberService.save(this.form.value).subscribe({
      next: (result) => {
        this.isLoading.set(false);
        this.messageService.add({
          severity: 'success',
          summary: 'Success',
          detail: 'Member saved successfully'
        });
        this.dialogRef.close(result);
      },
      error: (err: HttpErrorResponse) => {
        this.isLoading.set(false);
        this.handleError(err);
      }
    });
  }

  private handleError(error: HttpErrorResponse): void {
    // Parse field-level validation errors
    if (error.status === 400) {
      this.parseAndSetBackendFieldErrors(error);
    }

    // Set general error message
    const message = error.error?.error || error.error?.message || 'An error occurred';
    this.errorMessage.set(message);
  }

  private parseAndSetBackendFieldErrors(error: HttpErrorResponse): void {
    const fieldErrors: Record<string, string[]> = {};

    if (error.error && typeof error.error === 'object') {
      for (const key of Object.keys(error.error)) {
        // Skip non-field keys
        if (['message', 'status', 'timestamp', 'path', 'error'].includes(key)) {
          continue;
        }
        const value = error.error[key];
        if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'string') {
          fieldErrors[key] = value;
        }
      }
    }

    this.backendFieldErrors.set(fieldErrors);
  }

  getBackendFieldErrors(fieldName: string): string[] {
    return this.backendFieldErrors()[fieldName] || [];
  }

  private clearErrors(): void {
    this.errorMessage.set(null);
    this.backendFieldErrors.set({});
  }
}
```

### Error Display in Templates
```html
<!-- General error banner -->
@if (errorMessage()) {
  <div class="error-banner">
    <i class="pi pi-exclamation-circle"></i>
    <span>{{ errorMessage() }}</span>
    <button (click)="errorMessage.set(null)" class="close-btn">
      <i class="pi pi-times"></i>
    </button>
  </div>
}

<!-- Field-level errors -->
<div class="form-field">
  <label for="email">Email <span class="required">*</span></label>
  <input id="email" formControlName="email" pInputText>

  <!-- Frontend validation errors -->
  @if (form.get('email')?.invalid && form.get('email')?.touched) {
    @if (form.get('email')?.errors?.['required']) {
      <div class="error-message">Email is required</div>
    }
    @if (form.get('email')?.errors?.['email']) {
      <div class="error-message">Invalid email format</div>
    }
  }

  <!-- Backend validation errors -->
  @for (error of getBackendFieldErrors('email'); track error) {
    <div class="error-message backend-error">{{ error }}</div>
  }
</div>
```

## Error Styling

### Error Message
```css
.error-message {
  color: #dc2626;
  font-size: 0.75rem;
  margin-top: 0.25rem;
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.error-message::before {
  content: '\e936'; /* pi-exclamation-circle */
  font-family: 'primeicons';
}
```

### Backend Error (Distinguished)
```css
.error-message.backend-error {
  color: #dc2626;
  background: #fef2f2;
  padding: 0.375rem 0.625rem;
  border-radius: 0.375rem;
  border: 1px solid #fee2e2;
  margin-top: 0.375rem;
}
```

### Error Banner
```css
.error-banner {
  background: #fef2f2;
  border: 1px solid #fee2e2;
  border-radius: 0.75rem;
  padding: 1rem;
  color: #dc2626;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

.error-banner i {
  font-size: 1.25rem;
}

.error-banner .close-btn {
  margin-left: auto;
  background: none;
  border: none;
  color: #dc2626;
  cursor: pointer;
}
```

### Input Error State
```css
input.ng-invalid.ng-touched,
.has-error input {
  border-color: #ef4444;
}

input.ng-invalid.ng-touched:focus {
  box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
```

## Error Scenarios

### Network Errors
```typescript
catchError((error) => {
  if (error.status === 0) {
    this.messageService.add({
      severity: 'error',
      summary: 'Connection Error',
      detail: 'Unable to connect to server. Check your internet connection.'
    });
  }
  return throwError(() => error);
})
```

### Timeout Errors
```typescript
this.http.get('/api/data').pipe(
  timeout(30000),
  catchError((error) => {
    if (error.name === 'TimeoutError') {
      this.messageService.add({
        severity: 'warn',
        summary: 'Request Timeout',
        detail: 'The request took too long. Please try again.'
      });
    }
    return throwError(() => error);
  })
);
```

### Optimistic Update Rollback
```typescript
// Save original state
const originalData = [...this.members()];

// Optimistically update
this.members.update(m => m.filter(x => x.id !== id));

// If error, rollback
this.memberService.delete(id).subscribe({
  error: () => {
    this.members.set(originalData);
    this.messageService.add({
      severity: 'error',
      summary: 'Error',
      detail: 'Failed to delete. Changes reverted.'
    });
  }
});
```

## Best Practices

### DO:
- Always show user-friendly error messages
- Log detailed errors to console for debugging
- Preserve form input when errors occur
- Clear errors when user starts fixing
- Provide actionable guidance when possible

### DON'T:
- Show stack traces to users
- Use generic "Something went wrong" without context
- Clear form data on validation errors
- Ignore 401/403 errors (handle auth properly)
- Swallow errors silently

