Skip to main content

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

  1. Use error boundaries for all pages and major components
  2. Throw structured errors with meaningful context
  3. Handle errors at the right level - don't catch too early
  4. Provide user-friendly messages via toastError
  5. Log errors with context for debugging
  6. Test error states during development
  7. Use fallback UI for better UX during errors
  8. 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)
}
}
}