Steven's Knowledge
Troubleshooting

Error Handling

Error handling strategies, debugging techniques, and crash recovery for frontend applications

Error Handling Troubleshooting

Effective error handling is the difference between a professional application and one that frustrates users. This guide covers common error handling failures and systematic solutions.


1. Unhandled Promise Rejections

Problem

// Promise rejection goes unhandled — silently fails
fetch('/api/data').then(res => res.json()).then(data => setData(data));

// User sees no error, but data never loads
// Console: Unhandled promise rejection: TypeError: Failed to fetch

Root Cause

  • Missing .catch() on promises
  • async function errors not caught
  • Event handlers swallowing errors

Solution

Always handle errors in async operations:

// With async/await
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    return await response.json();
  } catch (error) {
    if (error instanceof TypeError) {
      // Network error — no connectivity
      showNotification('Network error. Please check your connection.');
    } else {
      // Server error
      showNotification('Failed to load data. Please try again.');
    }
    reportError(error);
    return null;
  }
}

Global unhandled rejection handler:

// Catch-all for unhandled rejections
window.addEventListener('unhandledrejection', (event) => {
  event.preventDefault(); // Prevent console error
  reportError({
    type: 'unhandled_rejection',
    reason: event.reason,
    promise: event.promise,
  });
});

// Catch-all for uncaught errors
window.addEventListener('error', (event) => {
  reportError({
    type: 'uncaught_error',
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    colno: event.colno,
    error: event.error,
  });
});

2. Network Error Handling — No Retry or Timeout

Problem

  • API calls fail silently on network issues
  • No retry mechanism for transient failures
  • Requests hang indefinitely without timeout
  • Users stuck on loading state forever

Solution

Fetch wrapper with timeout and retry:

async function fetchWithRetry(
  url: string,
  options: RequestInit & { retries?: number; timeout?: number } = {}
): Promise<Response> {
  const { retries = 3, timeout = 10000, ...fetchOptions } = options;

  for (let attempt = 0; attempt <= retries; attempt++) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
      const response = await fetch(url, {
        ...fetchOptions,
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      if (response.ok) return response;

      // Don't retry client errors (4xx)
      if (response.status >= 400 && response.status < 500) {
        throw new HttpError(response.status, await response.text());
      }

      // Retry server errors (5xx)
      if (attempt === retries) {
        throw new HttpError(response.status, 'Max retries exceeded');
      }
    } catch (error) {
      clearTimeout(timeoutId);

      if (error instanceof HttpError) throw error;

      if (attempt === retries) throw error;

      // Exponential backoff
      await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
    }
  }

  throw new Error('Unreachable');
}

class HttpError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = 'HttpError';
  }
}

TanStack Query built-in retry:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 3,
      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
      staleTime: 5 * 60 * 1000,
    },
  },
});

3. Error Boundaries Don't Catch Everything

Problem

React Error Boundaries don't catch:

  • Errors in event handlers
  • Errors in async code (promises, setTimeout)
  • Errors in server-side rendering
  • Errors thrown in the error boundary itself

Solution

Layer error handling strategies:

// 1. Error Boundary — catches render errors
<ErrorBoundary fallback={<ErrorPage />}>
  <App />
</ErrorBoundary>

// 2. Event handler errors — try/catch
function handleSubmit() {
  try {
    processForm();
  } catch (error) {
    setError(error instanceof Error ? error.message : 'Unknown error');
    reportError(error);
  }
}

// 3. Async errors — caught by data fetching libraries
const { error } = useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
  // TanStack Query catches async errors automatically
});

// 4. Global fallback
window.addEventListener('error', globalErrorHandler);
window.addEventListener('unhandledrejection', globalRejectionHandler);

Custom hook for error handling in event handlers:

function useErrorHandler() {
  const [error, setError] = useState<Error | null>(null);

  // Throw in render to trigger Error Boundary
  if (error) throw error;

  const handleError = useCallback((error: unknown) => {
    setError(error instanceof Error ? error : new Error(String(error)));
  }, []);

  const wrapAsync = useCallback(
    <T extends (...args: any[]) => Promise<any>>(fn: T) =>
      async (...args: Parameters<T>) => {
        try {
          return await fn(...args);
        } catch (error) {
          handleError(error);
        }
      },
    [handleError]
  );

  return { handleError, wrapAsync };
}

// Usage
function UploadForm() {
  const { wrapAsync } = useErrorHandler();

  const handleUpload = wrapAsync(async (file: File) => {
    await uploadFile(file); // If this throws, Error Boundary catches it
  });

  return <button onClick={() => handleUpload(file)}>Upload</button>;
}

4. Poor Error Messages — Users Don't Know What to Do

Problem

Error: Something went wrong
Error: null
Error: [object Object]
Error: 500

Users see cryptic messages with no actionable guidance.

Solution

User-facing vs developer-facing errors:

// Error classification
class AppError extends Error {
  constructor(
    public userMessage: string,     // What the user sees
    public technicalMessage: string, // What developers see in logs
    public code: string,             // Machine-readable error code
    public isRetryable: boolean = false,
  ) {
    super(technicalMessage);
    this.name = 'AppError';
  }
}

// Error factory
const errors = {
  networkError: () => new AppError(
    'Unable to connect. Please check your internet connection and try again.',
    'Network request failed — possible DNS/connectivity issue',
    'NETWORK_ERROR',
    true,
  ),
  notFound: (resource: string) => new AppError(
    `The ${resource} you're looking for could not be found.`,
    `404: Resource ${resource} not found`,
    'NOT_FOUND',
    false,
  ),
  serverError: () => new AppError(
    'Our servers are experiencing issues. Please try again in a few minutes.',
    'Server returned 5xx status',
    'SERVER_ERROR',
    true,
  ),
};

Error UI with clear actions:

function ErrorDisplay({ error }: { error: AppError }) {
  return (
    <div role="alert" className="error-container">
      <h2>Oops!</h2>
      <p>{error.userMessage}</p>
      <div className="error-actions">
        {error.isRetryable && (
          <button onClick={() => window.location.reload()}>Try Again</button>
        )}
        <button onClick={() => navigator.clipboard.writeText(error.code)}>
          Copy Error Code: {error.code}
        </button>
        <a href="/support">Contact Support</a>
      </div>
    </div>
  );
}

5. Console Errors in Production — No Visibility

Problem

  • Errors only visible in browser console — no production visibility
  • No alerting when error rate increases
  • Can't reproduce issues without user's browser

Solution

Structured error reporting:

// Error reporting service integration
interface ErrorReport {
  message: string;
  stack?: string;
  componentStack?: string;
  url: string;
  userAgent: string;
  timestamp: number;
  userId?: string;
  metadata: Record<string, unknown>;
}

function reportError(error: unknown, metadata: Record<string, unknown> = {}) {
  const report: ErrorReport = {
    message: error instanceof Error ? error.message : String(error),
    stack: error instanceof Error ? error.stack : undefined,
    url: window.location.href,
    userAgent: navigator.userAgent,
    timestamp: Date.now(),
    userId: getCurrentUserId(),
    metadata,
  };

  // Send to error tracking service (Sentry, Datadog, etc.)
  Sentry.captureException(error, {
    extra: metadata,
    tags: { component: metadata.component as string },
  });

  // Also log for local development
  if (import.meta.env.DEV) {
    console.error('[Error Report]', report);
  }
}

Source maps for readable stack traces (see Build & Deploy guide).


6. Debugging Difficult Issues

Problem

  • Bug only reproduces in production
  • Bug is intermittent and non-deterministic
  • Bug depends on specific user data or state

Solution

Structured debugging approach:

// 1. Reproduce: Add query-param debug mode
if (new URLSearchParams(location.search).has('debug')) {
  localStorage.setItem('debug', 'true');
}

// 2. Isolate: Conditional logging
const debug = {
  log: (...args: unknown[]) => {
    if (localStorage.getItem('debug') === 'true') {
      console.log('[DEBUG]', new Date().toISOString(), ...args);
    }
  },
  table: (data: unknown) => {
    if (localStorage.getItem('debug') === 'true') {
      console.table(data);
    }
  },
  trace: (label: string) => {
    if (localStorage.getItem('debug') === 'true') {
      console.trace(`[TRACE] ${label}`);
    }
  },
};

Session replay for production debugging:

Integrate session replay tools (LogRocket, FullStory, Sentry Replay) to see exactly what the user experienced:

import * as Sentry from '@sentry/react';

Sentry.init({
  dsn: 'https://...',
  integrations: [
    Sentry.replayIntegration({
      maskAllText: false,
      blockAllMedia: false,
    }),
  ],
  replaysSessionSampleRate: 0.1,  // 10% of sessions
  replaysOnErrorSampleRate: 1.0,  // 100% of error sessions
});

React DevTools Profiler for performance debugging:

import { Profiler } from 'react';

function onRender(
  id: string,
  phase: 'mount' | 'update',
  actualDuration: number,
) {
  if (actualDuration > 16) { // Slower than 60fps
    console.warn(`Slow render: ${id} took ${actualDuration}ms (${phase})`);
  }
}

<Profiler id="ExpensiveComponent" onRender={onRender}>
  <ExpensiveComponent />
</Profiler>

7. API Error Handling Inconsistency

Problem

Different parts of the application handle API errors differently:

  • Some show toast notifications
  • Some show inline errors
  • Some silently fail
  • Some redirect to error pages

Solution

Centralized API error handling layer:

// api/client.ts — single source of truth for API error handling
class APIClient {
  private baseURL: string;

  constructor(baseURL: string) {
    this.baseURL = baseURL;
  }

  async request<T>(endpoint: string, options?: RequestInit): Promise<T> {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...options?.headers,
      },
    });

    if (!response.ok) {
      const body = await response.json().catch(() => ({}));

      switch (response.status) {
        case 401:
          // Redirect to login
          window.location.href = '/login';
          throw new AppError('Session expired', 'AUTH_EXPIRED', false);
        case 403:
          throw new AppError('You don\'t have permission', 'FORBIDDEN', false);
        case 404:
          throw new AppError('Resource not found', 'NOT_FOUND', false);
        case 422:
          throw new ValidationError(body.errors);
        case 429:
          throw new AppError('Too many requests. Please wait.', 'RATE_LIMIT', true);
        default:
          throw new AppError('Server error. Please try again.', 'SERVER_ERROR', true);
      }
    }

    return response.json();
  }
}

Summary: Error Handling Architecture

                    ┌──────────────────────┐
                    │   Global Error       │
                    │   Handlers           │
                    │   (window.onerror)   │
                    └──────┬───────────────┘

                    ┌──────▼───────────────┐
                    │   Error Boundaries   │
                    │   (React render)     │
                    └──────┬───────────────┘

              ┌────────────┼────────────────┐
              │            │                │
       ┌──────▼──────┐ ┌──▼──────────┐ ┌───▼─────────┐
       │ API Layer   │ │ Event       │ │ Async       │
       │ try/catch   │ │ Handlers    │ │ Operations  │
       │ + retry     │ │ try/catch   │ │ AbortCtrl   │
       └─────────────┘ └─────────────┘ └─────────────┘
              │            │                │
              └────────────┼────────────────┘

                    ┌──────▼───────────────┐
                    │   Error Reporting    │
                    │   (Sentry, etc.)     │
                    └──────────────────────┘

On this page