Skip to content

错误边界与降级

优雅处理 React 应用中的错误

1. Error Boundary 基础

1.1 什么是 Error Boundary?

Error Boundary 是一种 React 组件,可以捕获子组件树中的 JavaScript 错误,记录错误,并显示降级 UI。

jsx
class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }
    
    static getDerivedStateFromError(error) {
        // 更新 state,下次渲染显示降级 UI
        return { hasError: true };
    }
    
    componentDidCatch(error, errorInfo) {
        // 记录错误信息
        logErrorToService(error, errorInfo.componentStack);
    }
    
    render() {
        if (this.state.hasError) {
            return <h1>Something went wrong.</h1>;
        }
        return this.props.children;
    }
}

1.2 使用方式

jsx
<ErrorBoundary>
    <MyComponent />
</ErrorBoundary>

// 嵌套使用,更细粒度的错误隔离
<ErrorBoundary fallback={<AppError />}>
    <Header />
    <ErrorBoundary fallback={<ContentError />}>
        <MainContent />
    </ErrorBoundary>
    <Footer />
</ErrorBoundary>

1.3 Error Boundary 不能捕获的错误

┌─────────────────────────────────────────────────────────────────┐
│                Error Boundary 无法捕获                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 事件处理器中的错误(需要 try-catch)                         │
│  2. 异步代码(setTimeout、fetch 等)                            │
│  3. 服务端渲染(SSR)                                           │
│  4. Error Boundary 自身的错误                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2. 实用 Error Boundary 实现

2.1 带重试功能

jsx
class ErrorBoundary extends React.Component {
    state = { hasError: false, error: null };
    
    static getDerivedStateFromError(error) {
        return { hasError: true, error };
    }
    
    componentDidCatch(error, errorInfo) {
        console.error('Error caught:', error, errorInfo);
    }
    
    handleReset = () => {
        this.setState({ hasError: false, error: null });
    };
    
    render() {
        if (this.state.hasError) {
            return this.props.fallback ? (
                this.props.fallback({
                    error: this.state.error,
                    reset: this.handleReset
                })
            ) : (
                <div>
                    <h2>出错了</h2>
                    <button onClick={this.handleReset}>重试</button>
                </div>
            );
        }
        return this.props.children;
    }
}

// 使用
<ErrorBoundary
    fallback={({ error, reset }) => (
        <div>
            <p>Error: {error.message}</p>
            <button onClick={reset}>重试</button>
        </div>
    )}
>
    <MyComponent />
</ErrorBoundary>

2.2 使用 react-error-boundary 库

jsx
import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
    return (
        <div role="alert">
            <p>Something went wrong:</p>
            <pre>{error.message}</pre>
            <button onClick={resetErrorBoundary}>Try again</button>
        </div>
    );
}

function App() {
    return (
        <ErrorBoundary
            FallbackComponent={ErrorFallback}
            onError={(error, info) => {
                // 上报错误
                logError(error, info);
            }}
            onReset={() => {
                // 重置应用状态
            }}
        >
            <MyComponent />
        </ErrorBoundary>
    );
}

// 在子组件中主动触发错误边界
function MyComponent() {
    const { showBoundary } = useErrorBoundary();
    
    const handleClick = async () => {
        try {
            await riskyOperation();
        } catch (error) {
            showBoundary(error);  // 触发最近的 Error Boundary
        }
    };
    
    return <button onClick={handleClick}>Do something risky</button>;
}

3. 函数组件中的错误处理

3.1 事件处理器错误

jsx
function MyComponent() {
    const handleClick = async () => {
        try {
            await riskyOperation();
        } catch (error) {
            // 方式一:显示错误状态
            setError(error);
            
            // 方式二:使用 Toast 通知
            showToast(error.message);
            
            // 方式三:上报错误
            reportError(error);
        }
    };
    
    return <button onClick={handleClick}>Click</button>;
}

3.2 useAsyncError Hook

jsx
// 将异步错误转换为渲染错误,让 Error Boundary 捕获
function useAsyncError() {
    const [, setError] = useState();
    
    return useCallback((error) => {
        setError(() => {
            throw error;  // 在渲染中抛出错误
        });
    }, []);
}

// 使用
function MyComponent() {
    const throwError = useAsyncError();
    
    useEffect(() => {
        fetchData()
            .catch(throwError);  // 异步错误会被 Error Boundary 捕获
    }, []);
    
    return <div>...</div>;
}

4. 降级策略

4.1 分层降级

jsx
// 全局降级
function App() {
    return (
        <ErrorBoundary fallback={<GlobalErrorPage />}>
            <Layout>
                {/* 页面级降级 */}
                <ErrorBoundary fallback={<PageErrorFallback />}>
                    <Routes>
                        <Route path="/" element={<HomePage />} />
                        <Route path="/dashboard" element={
                            {/* 组件级降级 */}
                            <ErrorBoundary fallback={<WidgetError />}>
                                <Dashboard />
                            </ErrorBoundary>
                        } />
                    </Routes>
                </ErrorBoundary>
            </Layout>
        </ErrorBoundary>
    );
}

4.2 优雅降级 vs 渐进增强

jsx
// 优雅降级:功能不可用时显示替代内容
function FeatureWithFallback() {
    const [error, setError] = useState(null);
    
    if (error) {
        return <SimplifiedVersion />;  // 降级到简化版本
    }
    
    return (
        <ErrorBoundary onError={setError}>
            <AdvancedFeature />
        </ErrorBoundary>
    );
}

// 渐进增强:基础功能始终可用,增强功能可选
function ProgressiveFeature() {
    const [enhanced, setEnhanced] = useState(false);
    
    useEffect(() => {
        // 检测浏览器是否支持增强功能
        if (supportsAdvancedFeature()) {
            setEnhanced(true);
        }
    }, []);
    
    return enhanced ? <EnhancedVersion /> : <BasicVersion />;
}

4.3 占位内容

jsx
// 内容不可用时显示占位
function ContentWithPlaceholder() {
    return (
        <ErrorBoundary
            fallback={
                <div className="placeholder">
                    <img src="/placeholder.svg" alt="" />
                    <p>内容暂时不可用</p>
                </div>
            }
        >
            <DynamicContent />
        </ErrorBoundary>
    );
}

5. 错误上报

5.1 错误信息收集

jsx
class ErrorBoundary extends React.Component {
    componentDidCatch(error, errorInfo) {
        // 收集错误信息
        const errorReport = {
            message: error.message,
            stack: error.stack,
            componentStack: errorInfo.componentStack,
            timestamp: new Date().toISOString(),
            userAgent: navigator.userAgent,
            url: window.location.href,
            userId: getCurrentUserId(),
        };
        
        // 上报到监控服务
        reportToSentry(errorReport);
        // 或其他服务
        // reportToLogRocket(errorReport);
    }
}

5.2 集成 Sentry

jsx
import * as Sentry from '@sentry/react';

// 初始化
Sentry.init({
    dsn: 'your-sentry-dsn',
    environment: process.env.NODE_ENV,
});

// 使用 Sentry 的 Error Boundary
function App() {
    return (
        <Sentry.ErrorBoundary fallback={<ErrorFallback />}>
            <MyApp />
        </Sentry.ErrorBoundary>
    );
}

// 手动上报
try {
    riskyOperation();
} catch (error) {
    Sentry.captureException(error);
}

6. 常见错误类型处理

6.1 网络错误

jsx
function DataFetcher() {
    const [data, setData] = useState(null);
    const [error, setError] = useState(null);
    const [retryCount, setRetryCount] = useState(0);
    
    const fetchData = async () => {
        try {
            const response = await fetch('/api/data');
            if (!response.ok) {
                throw new Error(`HTTP error: ${response.status}`);
            }
            const json = await response.json();
            setData(json);
            setError(null);
        } catch (err) {
            setError(err);
            
            // 自动重试(最多 3 次)
            if (retryCount < 3) {
                setTimeout(() => {
                    setRetryCount(c => c + 1);
                    fetchData();
                }, 1000 * Math.pow(2, retryCount));  // 指数退避
            }
        }
    };
    
    if (error && retryCount >= 3) {
        return <NetworkErrorUI error={error} onRetry={() => {
            setRetryCount(0);
            fetchData();
        }} />;
    }
    
    return data ? <DataDisplay data={data} /> : <Loading />;
}

6.2 权限错误

jsx
function ProtectedContent() {
    const { user, error } = useAuth();
    
    if (error?.code === 'UNAUTHORIZED') {
        return <Redirect to="/login" />;
    }
    
    if (error?.code === 'FORBIDDEN') {
        return <AccessDenied />;
    }
    
    return <SecretContent />;
}

6.3 表单验证错误

jsx
function Form() {
    const [errors, setErrors] = useState({});
    
    const handleSubmit = async (data) => {
        try {
            await submitForm(data);
        } catch (error) {
            if (error.validationErrors) {
                // 显示字段级错误
                setErrors(error.validationErrors);
            } else {
                // 显示通用错误
                showToast(error.message);
            }
        }
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <input name="email" />
            {errors.email && <span className="error">{errors.email}</span>}
            <button>Submit</button>
        </form>
    );
}

7. Next.js 中的错误处理

7.1 error.tsx

tsx
// app/dashboard/error.tsx
'use client';

export default function Error({
    error,
    reset,
}: {
    error: Error & { digest?: string };
    reset: () => void;
}) {
    useEffect(() => {
        // 上报错误
        console.error(error);
    }, [error]);
    
    return (
        <div>
            <h2>Something went wrong!</h2>
            <button onClick={reset}>Try again</button>
        </div>
    );
}

7.2 global-error.tsx

tsx
// app/global-error.tsx - 处理根布局错误
'use client';

export default function GlobalError({
    error,
    reset,
}: {
    error: Error & { digest?: string };
    reset: () => void;
}) {
    return (
        <html>
            <body>
                <h2>Something went wrong!</h2>
                <button onClick={reset}>Try again</button>
            </body>
        </html>
    );
}

8. 面试高频问题

Q1: Error Boundary 能捕获哪些错误?

能捕获

  • 子组件渲染期间的错误
  • 生命周期方法中的错误
  • 构造函数中的错误

不能捕获

  • 事件处理器
  • 异步代码(setTimeout, Promise)
  • 服务端渲染
  • Error Boundary 自身的错误

Q2: 为什么 Error Boundary 必须用 Class 组件?

因为 getDerivedStateFromErrorcomponentDidCatch 是 Class 组件的生命周期方法,目前没有对应的 Hooks。不过可以用 react-error-boundary 库在函数组件中使用。

Q3: 如何在事件处理器中处理错误?

jsx
const handleClick = async () => {
    try {
        await riskyOperation();
    } catch (error) {
        // 1. 设置错误状态
        setError(error);
        // 2. 显示 Toast
        showToast(error.message);
        // 3. 使用 useErrorBoundary
        showBoundary(error);
    }
};

Q4: 错误降级的最佳实践?

  1. 分层错误边界:全局、页面、组件三层
  2. 有意义的降级 UI:不要只显示"出错了"
  3. 提供重试机制:让用户可以恢复
  4. 保持部分功能可用:隔离错误影响范围
  5. 记录错误信息:便于排查问题

Q5: 如何测试 Error Boundary?

jsx
import { render, screen } from '@testing-library/react';

// 抑制 console.error
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});

test('renders fallback on error', () => {
    const ThrowError = () => {
        throw new Error('Test error');
    };
    
    render(
        <ErrorBoundary fallback={<div>Error occurred</div>}>
            <ThrowError />
        </ErrorBoundary>
    );
    
    expect(screen.getByText('Error occurred')).toBeInTheDocument();
});

spy.mockRestore();

前端面试知识库