一、为什么需要特别关注异步错误?
在前端监控系统里工作的某个深夜,我发现80%的线上崩溃日志都来自未被捕获的异步错误。传统的try/catch面对setTimeout回调里的报错束手无策,就像拿着渔网去抓空气里的灰尘。当我们开始用Promise时,如果没有正确的错误处理链,代码就会变成薛定谔的程序——运行成功与否完全不可预测。
// 经典陷阱示例:看起来安全的代码
function fetchUser() {
fetch('/api/user').then(response => {
// 即使响应状态码是500也会进入这里
return response.json();
});
}
// 正确的链式处理
function safeFetchUser() {
return fetch('/api/user')
.then(response => {
if (!response.ok) throw new Error('HTTP error');
return response.json();
})
.catch(error => {
console.error('请求失败:', error);
throw error; // 继续传递错误
});
}
二、全局异常捕获的三重门
2.1 浏览器环境的全域守护
// 错误捕获的黄金标准配置
window.addEventListener('error', event => {
// 注意:跨域脚本的错误信息会被浏览器隐藏
const { message, filename, lineno, colno, error } = event;
// 记录到监控系统
sendToMonitoring({
type: 'WINDOW_ERROR',
message,
stack: error?.stack,
location: `${filename}:${lineno}:${colno}`,
timestamp: Date.now()
});
// 阻止默认控制台输出(谨慎使用)
event.preventDefault();
});
2.2 Promise世界的最后防线
window.addEventListener('unhandledrejection', event => {
const reason = event.reason;
// 判断是否是业务预期错误
if (reason instanceof BusinessError) {
handleBusinessError(reason);
} else {
// 记录未知错误
captureUnexpectedError(reason);
}
// 避免浏览器默认提示
event.preventDefault();
});
// Node.js的附加配置
process.on('unhandledRejection', (reason, promise) => {
logger.fatal('未处理的Promise拒绝:', reason);
// 建议在此处执行进程重启
});
三、Promise错误处理的六大法则
3.1 拒绝幽灵Promise
// 危险的裸奔Promise
const dangerPromise = new Promise((resolve, reject) => {
doSomethingRisky();
});
// 安全的武装Promise
const safePromise = new Promise((resolve, reject) => {
try {
const result = doSomethingRisky();
resolve(result);
} catch (syncError) {
reject(new OperationError(syncError));
}
});
3.2 async/await的错误伪装术
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
return await validateResponse(response);
} catch (error) {
if (i === retries - 1) throw error;
await delay(1000 * (i + 1));
}
}
}
// 使用Wrapper函数统一处理
async function safeAwait(promise) {
try {
return await promise;
} catch (error) {
captureAsyncError(error);
throw error; // 保持错误传播
}
}
四、React世界的特殊防御
当遇到组件树的异步错误时,错误边界(Error Boundary)是React开发者必须掌握的防御性编程技术:
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
// 将组件堆栈信息发送到Sentry
Sentry.captureException(error, { extra: info });
}
render() {
if (this.state.hasError) {
return <FallbackUI />;
}
return this.props.children;
}
}
// 使用方式
<ErrorBoundary>
<AsyncComponent />
</ErrorBoundary>
五、错误处理进阶策略
5.1 错误类型体系设计
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
}
}
class ValidationError extends Error {
constructor(field, message) {
super(`字段验证失败: ${field}`);
this.field = field;
this.details = message;
}
}
// 错误处理中心
function errorHandler(error) {
if (error instanceof NetworkError) {
showToast(`网络错误: ${error.statusCode}`);
} else if (error instanceof ValidationError) {
highlightField(error.field);
} else {
reportUnknownError(error);
}
}
六、应用场景与技术选择
在微服务架构的前端应用中,全局错误处理尤其重要。比如电商系统需要:
- 自动捕获下单过程中的网络异常
- 记录用户操作流程中的验证错误
- 在支付失败时自动重试并触发通知
- 收集客户端异常用于优化用户体验
七、核心技术优缺点对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| window.onerror | 全局覆盖,简单直接 | 无法捕获Promise错误 |
| unhandledrejection | 专注异步错误 | 需要手动阻止默认行为 |
| Error Boundary | 组件级隔离 | 仅React生态可用 |
| 中间件封装 | 高度可定制 | 增加代码复杂度 |
八、工程师的防弹衣:注意事项
不要吞掉异常:即使在全局处理中捕获了错误,也要确保重新抛出
window.addEventListener('unhandledrejection', event => { handleError(event.reason); event.preventDefault(); // 阻止浏览器默认日志 });堆栈追踪的重要性:
new Error().stack; // 包含完整的调用栈 setTimeout(() => { throw new Error() }, 0); // 堆栈信息断裂Source Map映射:生产环境需配置正确的source map策略以确保可调试性
九、工程师的救生指南
当遇到诡异的"Uncaught (in promise)"错误时,按以下步骤排查:
- 检查Promise链是否完整
- 确认async函数内部都有try/catch
- 验证全局监听器是否正确定义
- 使用
Promise.prototype.catch()进行局部处理 - 在Node.js中启用
--unhandled-rejections=strict模式
十、总结
异步错误处理就像在代码世界安装烟雾报警器,全局捕获是我们的最后防线,Promise链式处理则是第一道防火墙。通过合理结合错误边界、类型系统、监控上报等手段,开发者可以建立起多层防御体系。记住,好的错误处理不是阻止错误发生,而是确保每个错误都走到它该去的地方。
Comments