算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
frontend-error-handling【设计与创意】Use this skill when the user says 'error handling', 'error boundary', 'error recovery', 'graceful degradation', 'fallback UI', 'error reporting', 'error logging', 'crash recovery', 'error fallback', 'retry pattern', 'error state', 'error boundary React', 'Vue error handler', 'Angular error handler', 'error boundary Svelte'. This skill enforces error …
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Catch, report, and recover from frontend errors without crashing the entire app. Error boundaries isolate failures. Users always see a functional fallback UI. Every error is reported to the monitoring service with context for debugging.
Exact phrases: "error handling", "error boundary", "error recovery", "graceful degradation", "fallback UI", "error reporting", "error logging", "crash recovery", "error state", "retry pattern", "error fallback".
Error boundary implementation, error reporting integration, fallback UI components, recovery patterns.
## Strategy
<boundary-locations, reporting-service, fallback-hierarchy>
## Implementation
<error-boundary-code, error-reporting-setup>
## Recovery
<retry-patterns, fallback-ui, reset-mechanism>
—
Compression footer: frontend-error-handling/v1 | boundaries: <count> | reporter: <service> | fallback: <full|partial>
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.
4096 tokens
Where does the error occur?
|-- Root layout / shell -->
| REQUIRE: RootErrorBoundary with full-page fallback + "Reload" button
| FAILSAFE: If RootBoundary itself crashes, show static error.html
|
|-- Route-level (page component) -->
| REQUIRE: RouteErrorBoundary with page-level fallback
| OPTION: Retry button calls router.push(sameRoute) to remount
|
|-- Widget / section (sidebar, widget, embedded component) -->
| REQUIRE: WidgetErrorBoundary with inline fallback
| BEHAVIOR: Hide widget, show degraded message, rest of page works
|
|-- Async operation (fetch, timeout, abort) -->
| REQUIRE: try/catch + throw to boundary OR inline error state
| OPTION: useErrorBoundary hook to throw from async code
|
|-- Event handler (onClick, onChange) -->
| REQUIRE: try/catch within handler -- boundaries DO NOT catch event handlers
| ACTION: Report error, show toast notification to user
|
|-- Third-party script (analytics, ads, embeds) -->
REQUIRE: Wrap in isolated sandbox (iframe) -- never let third-party JS crash host app
ACTION: Log warning, continue without the third-party feature
User sees error state.
|-- Is it a transient error? (network timeout, server 503) -->
| |-- YES: Show "Retry" button + auto-retry (up to 3 times with exponential backoff)
| |-- NO: Show "Try again" button, but don't auto-retry
|
|-- Is the error in a non-critical widget? -->
| |-- YES: Hide widget, degrade gracefully, rest of page works
| |-- NO: Show full-page or route-level fallback
|
|-- Has the user retried 3+ times without success? -->
|-- YES: Show permanent error screen with support contact info
|-- NO: Allow another retry
What type of error is this?
|-- Network error (fetch failed, timeout, abort) -->
| REPORT? NO -- too noisy, these are expected in poor connectivity
| ACTION: Track count as metric, not individual event
|
|-- Application error (TypeError, ReferenceError, null access) -->
| REPORT? YES -- with stack trace, component stack, breadcrumbs
| PRIORITY: medium (user-impacting)
|
|-- Third-party script error -->
| REPORT? CONDITIONAL -- only if it impacts user experience
| FILTER: Known third-party errors should be filtered in beforeSend
|
|-- AbortError (cancelled requests from rapid navigation) -->
REPORT? NO -- this is normal user behavior
FILTER: return null in Sentry beforeSend
<RootBoundary> ← catches anything not caught below: show full-page crash screen
<Layout>
<RouteBoundary> ← catches per-route errors: render route-specific fallback
<Outlet />
</RouteBoundary>
<WidgetBoundary> ← catches widget errors: show widget-level fallback
<Widget />
</WidgetBoundary>
</Layout>
</RootBoundary>
import { Component, ErrorInfo, ReactNode } from 'react'
interface Props { fallback?: ReactNode; onError?: (error: Error, info: ErrorInfo) => void; children: ReactNode }
interface State { hasError: boolean; error: Error | null }
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null }
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
componentDidCatch(error: Error, info: ErrorInfo) {
reportError(error, { componentStack: info.componentStack })
this.props.onError?.(error, info)
}
handleRetry = () => this.setState({ hasError: false, error: null })
render() {
if (this.state.hasError) {
return this.props.fallback ?? <DefaultFallback error={this.state.error} onRetry={this.handleRetry} />
}
return this.props.children
}
}
function DefaultFallback({ error, onRetry }: { error: Error | null; onRetry: () => void }) {
return (
<div role="alert" className="error-fallback">
<h2>Something went wrong</h2>
<p>{error?.message ?? 'An unexpected error occurred.'}</p>
<div className="error-actions">
<button onClick={onRetry}>Try again</button>
<button onClick={() => window.location.reload()}>Reload page</button>
<a href="/support">Contact support</a>
</div>
{process.env.NODE_ENV === 'development' && <pre>{error?.stack}</pre>}
</div>
)
}
function useAsyncError() {
const [, setError] = useState<Error | null>(null)
return (error: Error) => {
setError(() => { throw error }) // throw in render to trigger boundary
}
}
// Usage
function DataComponent() {
const throwError = useAsyncError()
const { data, error } = useQuery(...)
if (error) {
throwError(error) // caught by nearest error boundary
}
return <div>{data}</div>
}
import * as Sentry from '@sentry/react'
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1,
integrations: [Sentry.browserTracingIntegration()],
beforeSend(event) {
if (event.exception?.values?.[0]?.type === 'AbortError') return null
return event
},
})
// Use Sentry's ErrorBoundary
<Sentry.ErrorBoundary fallback={<ErrorFallback />}>
<App />
</Sentry.ErrorBoundary>
// Widget-level degradation
function WeatherWidget() {
const { data, error, isLoading } = useWeatherData()
if (isLoading) return <Skeleton width={200} height={100} />
if (error) return <WidgetError message="Weather unavailable" type="non-critical" />
return <WeatherDisplay data={data} />
}
// Non-critical widget error
function WidgetError({ message, type }: { message: string; type: 'critical' | 'non-critical' }) {
if (type === 'non-critical') {
return <div className="widget-degraded">{message}</div>
}
throw new Error(message) // Let boundary handle critical widgets
}
window.addEventListener('error', (event) => {
reportError(event.error ?? new Error(event.message), { type: 'uncaught-error' })
})
window.addEventListener('unhandledrejection', (event) => {
reportError(event.reason, { type: 'unhandled-promise-rejection' })
event.preventDefault()
})
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn()
} catch (error) {
if (attempt === maxRetries) throw error
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000
await new Promise(resolve => setTimeout(resolve, delay))
}
}
throw new Error('Unreachable')
}
import { createApp } from 'vue'
const app = createApp(App)
app.config.errorHandler = (error, instance, info) => {
reportError(error as Error, {
componentName: instance?.type?.name,
info, // e.g., "render function", "setup function"
})
}
app.config.warnHandler = (msg, instance, trace) => {
if (process.env.NODE_ENV === 'production') return
console.warn(msg, trace)
}
import { ErrorHandler, Injectable } from '@angular/core'
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
handleError(error: Error) {
reportError(error, { source: 'angular-global' })
console.error(error)
}
}
// providers: [{ provide: ErrorHandler, useClass: GlobalErrorHandler }]
function reportError(error: Error, context?: Record<string, unknown>) {
const enrichedContext = {
...context,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
route: window.location.pathname,
// DO NOT include: tokens, passwords, PII, API keys
}
if (window.__SENTRY__) {
Sentry.captureException(error, { extra: enrichedContext })
} else {
console.error('[ErrorReport]', error, enrichedContext)
}
}
// BAD -- silently swallowed error
try { await fetchData() } catch (e) { /* nothing */ }
// GOOD -- report and show user feedback
try { await fetchData() } catch (e) {
reportError(e)
showToast({ type: 'error', message: 'Failed to load data' })
}
Error boundaries do NOT catch errors in event handlers, setTimeout callbacks, or async/await (without the throw-in-render pattern). Always wrap event handlers in try/catch.
// BAD -- infinite retry
<button onClick={() => boundary.reset()}>Retry</button>
// Component crashes again immediately -> boundary catches -> user clicks retry -> infinite
// GOOD -- count retries
const [retryCount, setRetryCount] = useState(0)
if (retryCount >= 3) return <PermanentError />
Strip tokens, passwords, and PII before sending error reports. Use Sentry's beforeSend to sanitize.
Never let a boundary render nothing. Always provide a meaningful fallback UI with recovery options.
| Approach | Rendering Impact | Recovery UX | Reporting | Setup Complexity |
|---|---|---|---|---|
| Error Boundaries (React) | Full subtree replaced | Retry/reset within boundary | Manual | Low (class component) |
| Sentry ErrorBoundary | Full subtree replaced | Retry, feedback button | Automatic | Low (add wrapper) |
| Vue errorHandler | Global catch | Manual recovery in handler | Manual | Very low (config) |
| Angular ErrorHandler | Global catch | Manual recovery | Manual | Very low (provider) |
| Try/catch per component | No re-render | Inline error state | Manual | Medium (per-component) |
| Zustand/Vuex error store | Reactive state | Global error state | Manual | Medium |
Error boundaries use a class component wrapper which adds minimal overhead (~0.1KB per boundary). The heavy cost is the component stack trace generation in development. In production builds, component stack traces are not available.
Sentry's captureException is async and non-blocking. It does not affect rendering performance. However, breadcrumb collection adds ~50ms per interaction tracked. Configure breadcrumb limits to avoid memory growth.
Automatic retry should use exponential backoff. Retrying every 1s for 30 retries creates 30 failed requests. With backoff: 1s, 2s, 4s, 8s, 16s = only 5 retries in 31 seconds.
role="alert" for screen reader announcementwindow.onerror -- it can leak cross-origin script detailsNo artifact produced unless requested.
Next skill: frontend-performance — error boundaries affect perceived performance, coordinate loading/error states.
Carry forward: error boundary placement, reporting service choice, fallback hierarchy.
` interface EventObserver { onEvent(event: T): Promise; }
class EventBus { private observers: Set<EventObserver> = new Set(); subscribe(observer: EventObserver): void { this.observers.add(observer); } unsubscribe(observer: EventObserver): void { this.observers.delete(observer); } async emit(event: T): Promise { const results = Array.from(this.observers).map(o => o.onEvent(event)); await Promise.allSettled(results); } } `
config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% over 5min | Critical | Page on-call |
| p99 latency | > 2s over 5min | Warning | Investigate |
| Throughput drop | > 50% over 1min | Critical | Check upstream |
| Queue depth | > 1000 over 1min | Warning | Scale consumers |
| Disk usage | > 85% | Warning | Clean or expand |
| Memory usage | > 90% heap | Critical | Restart or scale |
| Anti-Pattern | Symptom | Root Cause | Solution |
|---|---|---|---|
| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |
| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |
| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |
| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).
Is the component fetching data?
├── No → Does it render user-generated content?
│ ├── Yes → Wrap in error boundary with "This content failed to load"
│ └── No → Does it use third-party integration?
│ ├── Yes → Error boundary per third-party widget
│ └── No → No boundary needed (static content)
└── Yes → Add error boundary per data-fetching component
Can the error be recovered without reload?
├── Yes → Show retry button inside boundary
└── No → Show fallback UI with navigation options
Is the error user-impacting?
├── No → Log to console + metrics (warning level)
└── Yes → Is it recoverable?
├── Yes → Show inline error + retry, report as warning
└── No → Show fallback UI, report as critical error
Does the error expose sensitive info?
├── Yes → Sanitize before reporting, show generic message
└── No → Report full error with context