🔥 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:
- Preventive Layer: Implement type guards and validation for all inputs
- Detection Layer: Catch exceptions with try-catch and branch processing according to error types
- Recovery Layer: Automatic retry (up to 3 times, exponential backoff) and fallback processing
- User Notification: Display clear error messages and a retry button
- Logging: Record error details with structured logs
- 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.