Source:
ocean/docs/patterns/error-handling.md| ✏️ Edit on GitHub
Error Handling Patterns
Ocean provides comprehensive error handling through error boundaries, structured error classes, and automatic error reporting.
Error Boundaries
Three Types of Error Boundaries
// 1. General Error Boundary
import { ErrorBoundary } from '@/components/error-boundary'
<ErrorBoundary>
<YourComponent />
</ErrorBoundary>
// 2. Authentication Error Boundary
import { AuthErrorBoundary } from '@/components/error-boundary'
<AuthErrorBoundary>
<ProtectedComponent />
</AuthErrorBoundary>
// 3. GraphQL Error Boundary
import { GraphQLErrorBoundary } from '@/components/error-boundary'
<GraphQLErrorBoundary>
<GraphQLComponent />
</GraphQLErrorBoundary>
Higher-Order Component Pattern
The recommended way to add error handling to components:
import { withErrorBoundary } from '@/components/with-error-boundary'
// Basic usage
export default withErrorBoundary(MyComponent)
// With custom fallback UI
export default withErrorBoundary(MyComponent, {
fallback: <CustomErrorUI />,
onError: (error, errorInfo) => {
console.error('Component error:', error)
// Custom error handling
}
})
// With error type filtering
export default withErrorBoundary(MyComponent, {
errorFilter: (error) => {
// Only catch specific errors
return error.name === 'NetworkError'
}
})
Manual Error Boundary Control
import { useErrorBoundary } from '@/hooks/use-error-boundary'
function MyComponent() {
const { showBoundary } = useErrorBoundary()
const handleError = async () => {
try {
await riskyOperation()
} catch (error) {
// Manually trigger error boundary
showBoundary(error)
}
}
return <Button onClick={handleError}>Perform Action</Button>
}
Structured Error Classes
Built-in Error Types
import { ApiError, AuthError, NetworkError } from '@/lib/errors'
// API Error with status code
throw new ApiError('Resource not found', {
statusCode: 404,
endpoint: '/api/users/123',
method: 'GET',
})
// Authentication Error
throw new AuthError('Session expired', {
code: 'SESSION_EXPIRED',
userId: user.id,
})
// Network Error
throw new NetworkError('Connection timeout', {
url: 'https://api.example.com',
timeout: 30000,
})
Custom Error Classes
// Create domain-specific errors
export class BillingError extends Error {
constructor(
message: string,
public code: string,
public customerId?: string
) {
super(message)
this.name = 'BillingError'
}
}
// Usage
throw new BillingError('Payment method declined', 'PAYMENT_DECLINED', customer.id)
Toast Error Handling
toastError Helper
Consistent error notifications across the app:
import { toastError } from '@/lib/toast'
// Basic usage
try {
await apiCall()
} catch (error) {
toastError(error) // Automatically formats error message
}
// With custom message
toastError(error, {
title: 'Upload Failed',
description: 'Please check your file and try again',
})
// Error type specific handling
toastError(error, {
apiError: (err) => `API Error: ${err.statusCode}`,
networkError: () => 'Please check your connection',
fallback: 'An unexpected error occurred',
})
Integration with Mutations
const mutation = useMutationWithToast({
mutationFn: apiCall,
// Error handling is automatic
// But you can customize:
onError: (error) => {
if (error instanceof BillingError) {
toastError(error, {
title: 'Billing Error',
action: {
label: 'Update Payment',
onClick: () => router.push('/billing'),
},
})
}
},
})
Form Error Handling
Valibot Integration
import { loginSchema } from '@/schemas/auth'
// Schema validation errors are automatically displayed
const form = useForm({
validators: {
onChange: loginSchema
},
onSubmit: async ({ value }) => {
try {
await login(value)
} catch (error) {
if (error instanceof ApiError && error.statusCode === 401) {
form.setError('password', 'Invalid credentials')
} else {
toastError(error)
}
}
}
})
// Field-level error display
<form.Field name="email">
{(field) => (
<>
<Input {...field.getInputProps()} />
{field.state.meta.errors && (
<p className="text-sm text-destructive">
{field.state.meta.errors.join(', ')}
</p>
)}
</>
)}
</form.Field>
Error Recovery Patterns
Retry with Exponential Backoff
import { withRetry } from '@/lib/api-utils'
// Automatic retry for transient errors
const data = await withRetry(() => fetchData(), {
maxAttempts: 3,
shouldRetry: (error) => {
// Retry on network errors or 5xx status codes
return error instanceof NetworkError || (error instanceof ApiError && error.statusCode >= 500)
},
})
Fallback Values
// Query with fallback
const { data = DEFAULT_SETTINGS } = useGraphQLQuery(
GET_USER_SETTINGS,
{ userId },
{
onError: (error) => {
console.warn('Failed to load settings, using defaults', error)
}
}
)
// Component with error fallback
function UserProfile({ userId }: { userId: string }) {
return (
<ErrorBoundary fallback={<ProfileSkeleton />}>
<ProfileContent userId={userId} />
</ErrorBoundary>
)
}
Error Monitoring
Sentry Integration
All errors are automatically reported to Sentry with context:
// Automatic error capture in production
// Includes:
// - User context
// - Breadcrumbs
// - Stack traces
// - Environment info
// Manual error reporting with context
import * as Sentry from '@sentry/react'
Sentry.captureException(error, {
tags: {
component: 'BillingForm',
action: 'updatePayment',
},
extra: {
customerId: customer.id,
planId: plan.id,
},
})
Error Logging
// Development logging
if (process.env.NODE_ENV === 'development') {
console.error('Detailed error:', {
error,
component: 'UserProfile',
props: { userId },
state: currentState,
})
}
// Production logging via Sentry breadcrumbs
Sentry.addBreadcrumb({
category: 'api',
message: 'API call failed',
level: 'error',
data: { endpoint, statusCode },
})
Testing Error States
Development Tools
// Error Boundary Test Component (dev only)
import { ErrorBoundaryTest } from '@/components/with-error-boundary'
function MyPage() {
return (
<>
{process.env.NODE_ENV === 'development' && <ErrorBoundaryTest />}
<PageContent />
</>
)
}
// Trigger test errors
<Button onClick={() => { throw new Error('Test error') }}>
Test Error Boundary
</Button>
Best Practices
- Use error boundaries for all pages and major components
- Throw structured errors with meaningful context
- Handle errors at the right level - don't catch too early
- Provide user-friendly messages via toastError
- Log errors with context for debugging
- Test error states during development
- Use fallback UI for better UX during errors
- Implement retry logic for transient failures
Common Patterns
Loading and Error States
function DataComponent() {
const { data, isLoading, error } = useQuery()
if (isLoading) return <LoadingSkeleton />
if (error) return <ErrorMessage error={error} />
return <DataDisplay data={data} />
}
Global Error Handler
// In your app root
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason)
toastError(event.reason)
event.preventDefault()
})
Form Submission Errors
const handleSubmit = async (data: FormData) => {
try {
await submitForm(data)
toast.success('Form submitted successfully')
router.push('/success')
} catch (error) {
if (error instanceof ApiError) {
// Field-specific errors
const fieldErrors = error.getFieldErrors()
Object.entries(fieldErrors).forEach(([field, message]) => {
form.setError(field, message)
})
} else {
// General error
toastError(error)
}
}
}