🔥 A Tragedy in Production: The Cost of Insufficient Error Handling

Insufficient error handling in a production environment can lead to serious problems. Emergency alerts blared in Slack. Reports from users flooded in, stating, "The production site is blank and nothing is displayed."

💥 What Happened

Symptom: The entire application turned into a blank screen, making all operations impossible

Cause: A third-party API temporarily returned a 500 error, causing the data fetch code generated by Genspark to throw an exception. Because there was no mechanism to catch it, and React Error Boundaries were not set up, the entire app crashed.

Scope of Impact: Approximately 2 hours of service downtime, affecting hundreds of users

Lesson Learned: "Working code" and "production-ready code" are entirely different things

Since this incident, when developers request code generation from Genspark, they always explicitly instruct, "Please implement it including comprehensive error handling."

🎯 Basic Error Handling Strategy: The 3-Layer Defense Model

The error handling strategy built in collaboration with Genspark is an approach we call the "3-Layer Defense Model."

1. Preventive Layer: Prevent Errors

The most effective error handling is a design where errors do not occur in the first place.

// ❌ Bad example: No input validation function processUserData(data) { return data.name.toUpperCase(); // Error if data or data.name is undefined } // ✅ Good example: Type guards and early returns function processUserData(data: unknown): string | null { // Ensure safety with type guards if (!data || typeof data !== 'object') { console.warn('Invalid data provided:', data); return null; } // Check for existence of required properties if (!('name' in data) || typeof data.name !== 'string') { console.warn('Missing or invalid name property:', data); return null; } // Check for empty string if (data.name.trim() === '') { console.warn('Empty name provided'); return null; } return data.name.toUpperCase(); } // ✅ Even better example: Using validation libraries like Zod import { z } from 'zod'; const UserSchema = z.object({ name: z.string().min(1, 'Name is required'), email: z.string().email('Invalid email format'), age: z.number().int().positive().optional(), }); function processUserData(data: unknown): string | null { try { const validData = UserSchema.parse(data); return validData.name.toUpperCase(); } catch (error) { if (error instanceof z.ZodError) { console.error('Validation failed:', error.errors); } return null; } }

🛡️ Instruction Examples for Genspark (Preventive Layer)

"Please implement a data processing function with the following requirements:

  • Adhere to TypeScript's strict mode (strict: true)
  • Implement type guards for all input parameters
  • Use Zod validation schemas
  • Thoroughly check for null and undefined
  • Properly handle edge cases (empty arrays, empty strings, 0, false)

2. Detection Layer: Catch Errors Early

Errors that slip past the preventive layer should be detected as early as possible and handled appropriately.

// API call error handling pattern interface ApiResponse<T> { data?: T; error?: ApiError; } interface ApiError { code: string; message: string; details?: Record<string, unknown>; } async function fetchUserData(userId: string): Promise<ApiResponse<User>> { try { const response = await fetch(`/api/users/${userId}`); // Check HTTP status code if (!response.ok) { // Get error details const errorData = await response.json().catch(() => ({})); // Process based on status code switch (response.status) { case 400: return { error: { code: 'BAD_REQUEST', message: 'Invalid user ID format', details: errorData, }, }; case 401: return { error: { code: 'UNAUTHORIZED', message: 'Authentication required', details: errorData, }, }; case 404: return { error: { code: 'NOT_FOUND', message: `User ${userId} not found`, details: errorData, }, }; case 429: return { error: { code: 'RATE_LIMIT', message: 'Too many requests. Please try again later.', details: errorData, }, }; case 500: case 502: case 503: return { error: { code: 'SERVER_ERROR', message: 'Server is temporarily unavailable', details: errorData, }, }; default: return { error: { code: 'UNKNOWN_ERROR', message: `Unexpected error: ${response.status}`, details: errorData, }, }; } } // Parse response const data = await response.json(); // Data validation const validationResult = UserSchema.safeParse(data); if (!validationResult.success) { return { error: { code: 'INVALID_RESPONSE', message: 'Server returned invalid data format', details: { zodErrors: validationResult.error.errors }, }, }; } return { data: validationResult.data }; } catch (error) { // Network errors, timeouts, etc. if (error instanceof TypeError && error.message.includes('fetch')) { return { error: { code: 'NETWORK_ERROR', message: 'Network connection failed', details: { originalError: error.message }, }, }; } // Other unexpected errors return { error: { code: 'UNEXPECTED_ERROR', message: 'An unexpected error occurred', details: { originalError: error instanceof Error ? error.message : String(error), }, }, }; } } // Usage example async function displayUserProfile(userId: string) { const result = await fetchUserData(userId); if (result.error) { // Handle based on error code switch (result.error.code) { case 'NOT_FOUND': showNotFoundPage(); break; case 'UNAUTHORIZED': redirectToLogin(); break; case 'NETWORK_ERROR': showRetryDialog(); break; default: showGenericErrorMessage(result.error.message); } return; } // Successful processing renderUserProfile(result.data); }

⚠️ Common Mistake: Silencing Errors

Please absolutely avoid code like the following:

// ❌ Worst pattern try { await someAsyncOperation(); } catch (error) { // Do nothing = silence the error } // ❌ Slightly better but insufficient try { await someAsyncOperation(); } catch (error) { console.log(error); // Only logs, no actual handling } // ✅ Appropriate handling try { await someAsyncOperation(); } catch (error) { // 1. Log recording logger.error('Operation failed', { error, context }); // 2. Notify user showErrorNotification('Operation failed. Please try again.'); // 3. Recovery process or fallback await fallbackOperation(); }

3. Recovery Layer: Recover from Errors

After an error is detected, how to recover the system is most important.

// Fetch function with automatic retry capability interface RetryOptions { maxRetries: number; initialDelay: number; maxDelay: number; backoffMultiplier: number; retryableStatuses: number[]; } const DEFAULT_RETRY_OPTIONS: RetryOptions = { maxRetries: 3, initialDelay: 1000, // 1 second maxDelay: 10000, // 10 seconds backoffMultiplier: 2, retryableStatuses: [408, 429, 500, 502, 503, 504], }; async function fetchWithRetry<T>( url: string, options: RequestInit = {}, retryOptions: Partial<RetryOptions> = {} ): Promise<T> { const config = { ...DEFAULT_RETRY_OPTIONS, ...retryOptions }; let lastError: Error | null = null; for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { const response = await fetch(url, options); // Check if it's a retryable status code if (!response.ok && config.retryableStatuses.includes(response.status)) { throw new Error(`HTTP ${response.status}`); } if (!response.ok) { // Non-retryable errors (e.g., 4xx series) const errorData = await response.json().catch(() => ({})); throw new Error(`HTTP ${response.status}: ${JSON.stringify(errorData)}`); } return await response.json(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // If it's the last attempt, throw an exception if (attempt === config.maxRetries) { throw lastError; } // Retry with exponential backoff const delay = Math.min( config.initialDelay * Math.pow(config.backoffMultiplier, attempt), config.maxDelay ); console.warn( `Request failed (attempt ${attempt + 1}/${config.maxRetries + 1}). ` + `Retrying in ${delay}ms...`, { error: lastError.message } ); await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError || new Error('Unknown error'); } // Data fetch with fallback capability async function fetchUserDataWithFallback(userId: string): Promise<User> { try { // Main API return await fetchWithRetry<User>(`/api/v2/users/${userId}`); } catch (primaryError) { console.warn('Primary API failed, trying fallback...', primaryError); try { // Fallback API (old version) return await fetchWithRetry<User>(`/api/v1/users/${userId}`); } catch (fallbackError) { console.warn('Fallback API failed, using cache...', fallbackError); try { // Get from cache const cachedData = await getCachedUser(userId); if (cachedData) { return cachedData; } } catch (cacheError) { console.error('Cache retrieval failed', cacheError); } // If all methods fail, throw an exception throw new Error( 'Failed to fetch user data from all sources. ' + 'Please check your connection and try again.' ); } } }

🎉 Effects of Implementing the Recovery Layer

As a result of implementing this 3-stage fallback mechanism:

  • Error Rate: User-perceived errors significantly reduced
  • Availability: Service continuity even during temporary API outages
  • User Satisfaction: Increased evaluations of "stable service"

🛡️ Error Handling for React Components

4. Error Boundary Implementation

In React applications, an Error Boundary to catch errors within the component tree is indispensable.

// ErrorBoundary.tsx import React, { Component, ErrorInfo, ReactNode } from 'react'; interface Props { children: ReactNode; fallback?: (error: Error, reset: () => void) => ReactNode; onError?: (error: Error, errorInfo: ErrorInfo) => void; } interface State { hasError: boolean; error: Error | null; } export class ErrorBoundary extends Component<Props, State> { constructor(props: Props) { super(props); this.state = { hasError: false, error: null, }; } static getDerivedStateFromError(error: Error): State { return { hasError: true, error, }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { // Send error logs console.error('ErrorBoundary caught an error:', error, errorInfo); // Call custom error handler this.props.onError?.(error, errorInfo); // Send to error monitoring service (e.g., Sentry) if (typeof window !== 'undefined' && window.Sentry) { window.Sentry.captureException(error, { contexts: { react: { componentStack: errorInfo.componentStack, }, }, }); } } resetError = () => { this.setState({ hasError: false, error: null, }); }; render() { if (this.state.hasError && this.state.error) { // Use custom fallback UI if available if (this.props.fallback) { return this.props.fallback(this.state.error, this.resetError); } // Default error UI return ( <div style={{ padding: '40px', textAlign: 'center', backgroundColor: '#fff5f5', border: '2px solid #fc8181', borderRadius: '8px', margin: '20px', }}> <h2 style={{ color: '#c53030', marginBottom: '16px' }}> An error occurred </h2> <p style={{ color: '#742a2a', marginBottom: '24px' }}> {this.state.error.message} </p> <button onClick={this.resetError} style={{ padding: '10px 20px', backgroundColor: '#3182ce', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', }} > Retry </button> </div> ); } return this.props.children; } } // Usage example function App() { return ( <ErrorBoundary fallback={(error, reset) => ( <CustomErrorPage error={error} onRetry={reset} /> )} onError={(error, errorInfo) => { logErrorToService({ error: error.message, stack: error.stack, componentStack: errorInfo.componentStack, }); }} > <AppContent /> </ErrorBoundary> ); }

5. Asynchronous Error Handling (Utilizing React Query)

By using React Query, data fetch error handling becomes dramatically easier.

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; // Integrate error handling with a custom hook function useUserData(userId: string) { return useQuery({ queryKey: ['user', userId], queryFn: () => fetchUserData(userId), retry: 3, retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), staleTime: 5 * 60 * 1000, // Use cache for 5 minutes onError: (error) => { console.error('Failed to fetch user:', error); // Notify error monitoring service reportError(error); }, }); } // Usage within a component function UserProfile({ userId }: { userId: string }) { const { data, isLoading, isError, error, refetch } = useUserData(userId); // Loading state if (isLoading) { return <LoadingSkeleton />; } // Error state if (isError) { return ( <ErrorDisplay title="Failed to retrieve user information" message={error instanceof Error ? error.message : 'Unknown error'} onRetry={refetch} /> ); } // If no data if (!data) { return <EmptyState message="User not found" />; } // Normal display return <UserCard user={data} />; } // Mutation error handling function useUpdateUser() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (userData: UpdateUserData) => updateUser(userData), onSuccess: (data, variables) => { // Update cache queryClient.setQueryData(['user', variables.userId], data); // Success notification showSuccessNotification('User information updated'); }, onError: (error, variables, context) => { // Error log console.error('Failed to update user:', error); // Notify user showErrorNotification('Update failed. Please try again.'); // Report to error monitoring service reportError(error); }, }); }

✨ Effects of Introducing React Query

  • Reduced Code Volume: Manual error handling code reduced by 70%
  • Consistency: Unified error handling across the entire app
  • Improved UX: Comfortable user experience with automatic retries and caching
  • Maintainability: Centralized management of error handling logic

📊 Error Logging and Monitoring

6. Structured Logging Implementation

In a production environment, logging that accurately captures the situation when an error occurs is essential.

// logger.ts enum LogLevel { DEBUG = 'DEBUG', INFO = 'INFO', WARN = 'WARN', ERROR = 'ERROR', } interface LogContext { userId?: string; sessionId?: string; requestId?: string; url?: string; userAgent?: string; [key: string]: unknown; } class Logger { private context: LogContext = {}; setContext(context: Partial<LogContext>) { this.context = { ...this.context, ...context }; } private log(level: LogLevel, message: string, data?: unknown) { const logEntry = { timestamp: new Date().toISOString(), level, message, context: this.context, data, }; // Output to console console.log(JSON.stringify(logEntry)); // In production, send to external service if (process.env.NODE_ENV === 'production') { this.sendToLoggingService(logEntry); } } debug(message: string, data?: unknown) { this.log(LogLevel.DEBUG, message, data); } info(message: string, data?: unknown) { this.log(LogLevel.INFO, message, data); } warn(message: string, data?: unknown) { this.log(LogLevel.WARN, message, data); } error(message: string, error: Error | unknown, data?: unknown) { const errorData = { ...data, error: { message: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, name: error instanceof Error ? error.name : undefined, }, }; this.log(LogLevel.ERROR, message, errorData); } private async sendToLoggingService(logEntry: unknown) { try { await fetch('/api/logs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logEntry), }); } catch (error) { // Silence logging failures (to prevent infinite loops) console.error('Failed to send log:', error); } } } export const logger = new Logger(); // Usage example async function processOrder(orderId: string) { logger.setContext({ orderId, userId: getCurrentUserId() }); try { logger.info('Starting order processing'); const order = await fetchOrder(orderId); logger.debug('Order fetched successfully', { order }); await validateOrder(order); logger.debug('Order validated'); await processPayment(order); logger.info('Payment processed successfully'); return order; } catch (error) { logger.error('Order processing failed', error, { stage: 'unknown', orderId, }); throw error; } }

🎯 Best Practices for Instructing Genspark

✅ Instruction Template for Implementing Comprehensive Error Handling

"Please implement the XXX feature with the following requirements:

  1. Preventive Layer: Implement type guards and validation for all inputs
  2. Detection Layer: Catch exceptions with try-catch and branch processing according to error types
  3. Recovery Layer: Automatic retry (up to 3 times, exponential backoff) and fallback processing
  4. User Notification: Display clear error messages and a retry button
  5. Logging: Record error details with structured logs
  6. Monitoring Integration: Notify monitoring services of critical errors

Return appropriate HTTP status codes for each error case, and ensure user feedback is implemented."

💫 Summary: Error Handling is Part of the User Experience

The biggest lesson learned from the production outage mentioned at the beginning is that "error handling is not a technical challenge, but a part of the user experience."

Perfect code does not exist. What's important is how the system behaves when an error occurs. By implementing appropriate error handling, users can feel secure even if unexpected problems arise, and service reliability can be maintained.

When requesting code generation from Genspark, by clearly specifying not only feature implementation but also the error handling strategy, code that can be operated with confidence in a production environment will be generated.

Next Steps: Review your existing codebase and try to identify areas where error handling is insufficient. Then, by applying the 3-layer defense model introduced in this article, you should be able to evolve your application into a more robust one.