前端错误处理的最佳实践与实战策略
常见误区
很多开发者认为加个 try-catch 就能搞定一切,或者觉得 console.error 足够记录日志。实际上,这往往导致生产环境排查困难,甚至让应用静默失败。
比如忽略 Promise 的 reject,或者在嵌套逻辑中过度使用 try-catch,不仅代码臃肿,还容易掩盖真正的异常来源。全局错误监听如果只打印到控制台,用户端完全无感知,这对线上稳定性是巨大的隐患。
反面案例
// 1. 忽略错误:Promise 未捕获的 reject 会导致静默失败
function fetchData() {
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data));
}
// 2. 过度嵌套:层层包裹的 try-catch 让逻辑难以阅读
function processData(data) {
try {
if (data) {
try {
const processedData = data.map(item => {
try {
return item.value * 2;
} catch (error) {
console.error('Error processing item:', error);
return 0;
}
});
return processedData;
} catch (error) {
console.error('Error mapping data:', error);
return [];
}
} else {
return [];
}
} catch (error) {
console.error('Error processing data:', error);
return [];
}
}
// 3. 错误信息不明确:无法定位具体原因
function calculateTotal(price, quantity) {
if (typeof price !== 'number') {
throw new Error('Invalid input');
}
return price * quantity;
}
// 4. 缺少全局错误处理:无法捕获运行时异常
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
});
问题点:
- 忽略异步错误,导致应用崩溃或数据不一致
- 过度防御性编程,增加维护成本
- 错误堆栈丢失关键上下文
- 缺乏统一的全局监控入口
正确的做法
分层错误处理策略
不要把所有逻辑都塞进一个 try-catch 块。建议采用分层处理:底层负责网络转换,中层负责业务校验,上层负责 UI 反馈。
// 1. 基础网络请求封装
async function apiCall(url, options) {
try {
const response = await fetch(url, options);
if (!response.ok) {
// 自定义错误类,携带状态码便于区分
throw new ApiError(`API error: ${response.status}`, response.status);
}
return await response.json();
} catch (error) {
// 网络错误统一包装
if (!(error instanceof ApiError)) {
throw new ApiError('Network error', 0);
}
throw error;
}
}
// 2. 业务层处理
async function getUserData(id) {
try {
return await apiCall(`/api/users/${id}`);
} catch (error) {
// 根据状态码做特定处理
if (error.status === 404) {
throw new Error('User not found');
}
throw error;
}
}
// 3. 自定义错误类定义
class ApiError extends Error {
constructor(message, status) {
super(message);
this.name = 'ApiError';
this.status = status;
}
}
React 错误边界
在 React 应用中,组件树中的错误不会自动冒泡到全局。使用 ErrorBoundary 可以隔离故障区域,防止整个应用白屏。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// 这里可以上报日志
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <div>Something went wrong. Please try again later.</div>;
}
return this.props.children;
}
}
全局监控与日志
除了浏览器原生事件,接入专业监控服务能提供更丰富的上下文(如用户操作路径、设备信息等)。
// 1. 全局监听
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
sendErrorToMonitoring(event.error);
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
sendErrorToMonitoring(event.reason);
});
// 2. 自定义上报函数
function sendErrorToMonitoring(error) {
const errorData = {
message: error.message,
stack: error.stack,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
};
// 实际项目中请替换为真实的上报接口
fetch('/api/error-monitoring', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData)
});
}
*注:若使用 Sentry 等第三方工具,初始化后通常只需调用 Sentry.captureException(error) 即可自动收集上述信息。
用户友好的交互反馈
错误不应直接抛给用户看堆栈信息。设计友好的提示页和重试机制能显著提升体验。
// 1. 通用错误提示组件
function ErrorMessage({ error, onRetry }) {
return (
<div className="error-message">
<h3>Oops! Something went wrong.</h3>
<p>{error?.message || 'Unknown error occurred'}</p>
{onRetry && <button onClick={onRetry}>Try Again</button>}
</div>
);
}
// 2. 带重试逻辑的数据获取
function DataFetcher() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Failed to fetch data');
const data = await response.json();
setData(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <ErrorMessage error={error} onRetry={() => window.location.reload()} />;
return <div>{/* 渲染数据 */}</div>;
}
总结与建议
错误处理的核心在于平衡。一方面要保证应用的健壮性,另一方面不能因为过度防御而牺牲代码的可读性和开发效率。
对于小型项目,简单的 try-catch 配合全局监听可能就够了;但对于大型应用,建议建立统一的错误规范,包括自定义错误类型、标准化的日志格式以及分级上报机制。记住,好的错误处理不是为了炫技,而是为了让用户在遇到问题时依然能感受到产品的可靠性。
