Error Handling Best Practices

Build resilient applications with proper error handling, retry logic, and user-friendly feedback.

Error Boundary

Wrap SDK components in an error boundary to prevent crashes from propagating:

ErrorBoundary.tsxtsx
import { Component, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
  onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

export class SDKErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log to monitoring service
    console.error('SDK Error:', error, errorInfo);
    this.props.onError?.(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="p-8 text-center">
          <h2 className="text-xl font-bold mb-2">Something went wrong</h2>
          <p className="text-neutral-600 mb-4">
            We encountered an error loading the document viewer.
          </p>
          <button
            onClick={() => this.setState({ hasError: false, error: null })}
            className="px-4 py-2 bg-emerald-500 text-white rounded"
          >
            Try Again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Usage
<SDKErrorBoundary onError={logToSentry}>
  <MIMSProvider apiKey={apiKey}>
    <DocumentViewer />
  </MIMSProvider>
</SDKErrorBoundary>

SDK Initialization Errors

Handle initialization errors using the onError callback:

Initialization Handlingtsx
function App() {
  const [initError, setInitError] = useState<string | null>(null);

  const handleError = (error: string) => {
    setInitError(error);
    
    // Log for debugging
    console.error('[MIMS SDK Init Error]', error);
    
    // Report to monitoring
    reportError(new Error(error), { context: 'sdk_init' });
  };

  if (initError) {
    return (
      <InitializationError 
        error={initError}
        onRetry={() => {
          setInitError(null);
          window.location.reload();
        }}
      />
    );
  }

  return (
    <MIMSProvider 
      apiKey={process.env.NEXT_PUBLIC_MIMS_API_KEY!}
      onError={handleError}
    >
      <DocumentApp />
    </MIMSProvider>
  );
}

function InitializationError({ error, onRetry }: { error: string; onRetry: () => void }) {
  const getMessage = () => {
    if (error.includes('API key')) {
      return 'Invalid configuration. Please check your API key.';
    }
    if (error.includes('network') || error.includes('timeout')) {
      return 'Network error. Please check your connection.';
    }
    return 'Failed to initialize. Please try again.';
  };

  return (
    <div className="p-8 text-center">
      <AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
      <h2 className="text-xl font-bold mb-2">Initialization Failed</h2>
      <p className="text-neutral-600 mb-4">{getMessage()}</p>
      <button onClick={onRetry} className="px-4 py-2 bg-black text-white rounded">
        Retry
      </button>
    </div>
  );
}

API Request Errors

Handle API errors with specific error types:

API Error Handlingtsx
// Custom error types
class APIError extends Error {
  constructor(
    message: string,
    public code: string,
    public status: number,
    public details?: Record<string, unknown>
  ) {
    super(message);
    this.name = 'APIError';
  }
}

// Error handler
function handleAPIError(error: unknown): string {
  if (error instanceof APIError) {
    switch (error.code) {
      case 'INVALID_API_KEY':
        return 'Authentication failed. Please check your API key.';
      case 'DOCUMENT_NOT_FOUND':
        return 'This document could not be found.';
      case 'INSUFFICIENT_PERMISSIONS':
        return 'You do not have permission to access this document.';
      case 'VALIDATION_ERROR':
        return 'Please check your input and try again.';
      case 'RATE_LIMIT_EXCEEDED':
        return 'Too many requests. Please wait a moment.';
      default:
        return 'An unexpected error occurred.';
    }
  }
  
  if (error instanceof Error) {
    if (error.message.includes('timeout')) {
      return 'Request timed out. Please try again.';
    }
    if (error.message.includes('network')) {
      return 'Network error. Please check your connection.';
    }
  }
  
  return 'An unexpected error occurred.';
}

// Usage in component
async function loadDocument(documentId: string) {
  try {
    const doc = await apiRequest<DocumentWithFields>(`/sdk/documents/${documentId}`);
    return doc;
  } catch (error) {
    const message = handleAPIError(error);
    showToast({ type: 'error', message });
    throw error; // Re-throw for error boundary
  }
}

Retry Logic

Implement exponential backoff for transient failures:

Retry with Backofftsx
interface RetryOptions {
  maxAttempts?: number;
  baseDelay?: number;
  maxDelay?: number;
  shouldRetry?: (error: unknown, attempt: number) => boolean;
}

async function withRetry<T>(
  fn: () => Promise<T>,
  options: RetryOptions = {}
): Promise<T> {
  const {
    maxAttempts = 3,
    baseDelay = 1000,
    maxDelay = 10000,
    shouldRetry = (error) => {
      // Don't retry client errors (4xx)
      if (error instanceof APIError && error.status >= 400 && error.status < 500) {
        return false;
      }
      return true;
    },
  } = options;

  let lastError: unknown;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      
      if (attempt === maxAttempts || !shouldRetry(error, attempt)) {
        throw error;
      }

      // Exponential backoff with jitter
      const delay = Math.min(
        baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000,
        maxDelay
      );
      
      console.log(`Retry attempt ${attempt} after ${delay}ms`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw lastError;
}

// Usage
const document = await withRetry(
  () => apiRequest<DocumentWithFields>(`/sdk/documents/${documentId}`),
  { maxAttempts: 3 }
);

Validation Errors

Handle field validation errors with clear user feedback:

Validation Error Displaytsx
function FieldWithValidation({ fieldId }: { fieldId: string }) {
  const field = useDocumentStore((s) => s.fields.get(fieldId));
  
  if (!field) return null;

  const hasErrors = field.errors.length > 0;
  const showErrors = field.touched && hasErrors;

  return (
    <div className="field-container">
      <label className="block text-sm font-medium mb-1">
        {field.definition.label}
        {field.definition.required && <span className="text-red-500 ml-1">*</span>}
      </label>
      
      <input
        className={`
          w-full px-3 py-2 border rounded
          ${showErrors ? 'border-red-500 focus:ring-red-500' : 'border-neutral-300'}
        `}
        aria-invalid={showErrors}
        aria-describedby={showErrors ? `${fieldId}-errors` : undefined}
      />
      
      {showErrors && (
        <ul 
          id={`${fieldId}-errors`}
          className="mt-1 text-sm text-red-500"
          role="alert"
        >
          {field.errors.map((error, i) => (
            <li key={i}>{error}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

// Validation summary before submit
function ValidationSummary() {
  const fields = useDocumentStore((s) => s.fields);
  const goToPage = useDocumentStore((s) => s.goToPage);
  const selectField = useDocumentStore((s) => s.selectField);

  const fieldsWithErrors = Array.from(fields.values())
    .filter(f => f.errors.length > 0 || (f.definition.required && !f.value));

  if (fieldsWithErrors.length === 0) return null;

  return (
    <div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
      <h3 className="font-semibold text-red-800 mb-2">
        Please fix the following issues:
      </h3>
      <ul className="space-y-1">
        {fieldsWithErrors.map((field) => (
          <li key={field.definition.id}>
            <button
              onClick={() => {
                goToPage(field.definition.page);
                selectField(field.definition.id);
              }}
              className="text-red-600 hover:underline text-sm"
            >
              {field.definition.label || field.definition.id}
              {field.errors.length > 0 
                ? `: ${field.errors[0]}`
                : ': Required field is empty'
              }
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Logging Best Practices

Error Loggingtsx
// Create a logging utility
const logger = {
  error: (message: string, error: unknown, context?: Record<string, unknown>) => {
    // Sanitize sensitive data
    const sanitizedContext = sanitize(context);
    
    // Log to console in development
    if (process.env.NODE_ENV === 'development') {
      console.error(message, error, sanitizedContext);
    }
    
    // Send to monitoring service in production
    if (process.env.NODE_ENV === 'production') {
      sendToMonitoring({
        level: 'error',
        message,
        error: error instanceof Error ? {
          name: error.name,
          message: error.message,
          stack: error.stack,
        } : String(error),
        context: sanitizedContext,
        timestamp: new Date().toISOString(),
      });
    }
  },
};

// Sanitize sensitive data
function sanitize(data: Record<string, unknown> | undefined) {
  if (!data) return undefined;
  
  const sensitiveKeys = ['apiKey', 'password', 'token', 'signature'];
  const sanitized = { ...data };
  
  for (const key of sensitiveKeys) {
    if (key in sanitized) {
      sanitized[key] = '[REDACTED]';
    }
  }
  
  return sanitized;
}

// Usage
try {
  await submitDocument(payload);
} catch (error) {
  logger.error('Document submission failed', error, {
    documentId: payload.documentId,
    fieldCount: payload.fields.length,
  });
  throw error;
}

Never Log Sensitive Data

Never log API keys, passwords, full signature images, or personally identifiable information (PII).

Related