如何优雅处理unhandledrejection避免线上事故

长孙玉研 前端 阅读 2,731
赞 21 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

最近刚结束一个电商类的H5项目,说实话挺折腾的。项目主要用Vue3开发,涉及到支付、商品展示这些常规功能。但让我印象最深的是处理未捕获的Promise异常这个问题。

如何优雅处理unhandledrejection避免线上事故

当时选择监听unhandledrejection主要是因为测试反馈说偶尔会白屏,但又复现不出来。后来发现是某些异步请求出错后没有被捕获导致的。为了能统一处理这些异常,我决定用unhandledrejection来兜底。

核心代码就这几行

实现起来其实不复杂,关键代码就下面这些:

window.addEventListener('unhandledrejection', function(event) {
    console.error('未捕获的Promise异常:', event.reason);
    
    // 防止异常继续冒泡
    event.preventDefault();

    // 统一错误处理逻辑
    handleGlobalError(event.reason);
});

function handleGlobalError(error) {
    if (error.response && error.response.status === 401) {
        // 处理未登录或token过期
        redirectToLogin();
    } else {
        // 其他错误上报到监控平台
        reportErrorToServer(error);
        showUserFriendlyMessage();
    }
}

function redirectToLogin() {
    // 跳转到登录页
    window.location.href = '/login';
}

function reportErrorToServer(error) {
    fetch('https://jztheme.com/api/error-report', {
        method: 'POST',
        body: JSON.stringify({
            message: error.message,
            stack: error.stack,
            timestamp: Date.now()
        })
    });
}

function showUserFriendlyMessage() {
    alert('系统出现异常,请稍后再试');
}

最大的坑:性能问题

开始没想太多,直接在监听器里加了错误上报的逻辑。结果上线后发现页面响应变慢了,特别是在网络状况不好的时候。仔细排查才发现,每次触发unhandledrejection都会发一个HTTP请求,而我们的错误监控服务器响应又比较慢。

折腾了半天才发现这个问题,最后改成用队列缓冲的方式处理:

let errorQueue = [];
let isSending = false;

function reportErrorToServer(error) {
    errorQueue.push(error);
    if (!isSending) processErrorQueue();
}

async function processErrorQueue() {
    if (errorQueue.length === 0) return;
    isSending = true;

    const batch = errorQueue.splice(0, 10); // 每次最多处理10个
    try {
        await fetch('https://jztheme.com/api/error-report-batch', {
            method: 'POST',
            body: JSON.stringify(batch.map(err => ({
                message: err.message,
                stack: err.stack,
                timestamp: Date.now()
            })))
        });
    } catch (e) {
        console.error('错误上报失败:', e);
    } finally {
        isSending = false;
        if (errorQueue.length > 0) processErrorQueue();
    }
}

这里注意我踩过好几次坑:一个是忘了处理并发问题,导致同时发起多个上报请求;另一个是没有做批量处理,单个错误都发请求太浪费资源。

谁更灵活?谁更省事?

中间还遇到个有意思的问题:有些第三方库也会抛出未捕获的Promise异常,比如某个图片懒加载库。最初我的方案是全部拦截,但这样会导致正常的功能也受到影响。

后来调整了下策略,在handleGlobalError里加了个白名单机制:

const IGNORED_ERRORS = [
    'Image load failed', // 某些图片加载失败可以忽略
    'LazyLoad timeout'   // 懒加载超时
];

function shouldIgnoreError(error) {
    return IGNORED_ERRORS.some(msg => error.message.includes(msg));
}

function handleGlobalError(error) {
    if (shouldIgnoreError(error)) return;

    // 原有的错误处理逻辑...
}

这个改动虽然简单,但确实让系统更健壮了。不过还是有个小问题没完全解决:有些动态加载的组件报错后,即使捕获了异常,界面还是会卡住。暂时还没找到特别好的解决方案,目前只能靠业务代码里多写try-catch来补救。

回顾与反思

总的来说,这次使用unhandledrejection的实践还算成功。最大的收获是建立了一套相对完整的异常监控体系,能及时发现生产环境的问题。特别是那个批量上报的优化,对性能提升很明显。

不过还是有几个点可以改进:

  • 错误分类还不够细,后续可以加个错误级别判断
  • 用户提示方式太生硬,应该做个更友好的toast提示
  • 对于某些特殊场景的异常处理还不够完善

以上是我个人对这个unhandledrejection的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,比如结合Sentry这类专业工具使用,后续我会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论