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).