3、JavaScript异常捕获:try-catch-finally、window.onerror、Promise异常处理、async/await异常处理

JavaScript的异常处理,说白了就是给代码上「保险」。

我做了这么多年混合开发,见过太多因为一个未捕获的异常导致整个页面白屏的惨案。你想想看,用户正用着你的App,突然页面卡死,那体验得多糟糕。

所以今天咱们就把JS异常捕获这件事彻底聊透。

3.1 try-catch-finally:最基础的防线

这是最经典的异常捕获方式。我个人习惯在可能出错的同步代码块外面包一层try-catch,就像给代码穿防弹衣。

try {
    // 可能会出错的代码
    let result = riskyOperation();
    console.log('操作成功:', result);
} catch (error) {
    // 出错后的处理
    console.error('捕获到异常:', error.message);
    // 上报异常
    reportError(error);
} finally {
    // 无论是否出错都会执行
    console.log('清理工作完成');
}

这里有个细节要注意:finally里的代码一定会执行,哪怕try或catch里有return语句。我在项目中遇到过这样一个坑——

我曾经踩过的坑:在finally里又抛了一个异常,结果把catch里捕获的异常给覆盖了。所以finally里尽量只做清理工作,别搞复杂逻辑。

场景 try执行 catch执行 finally执行
无异常
有异常 中断
try中有return ✅(return前执行)

3.2 window.onerror:全局的兜底网

try-catch只能捕获它包裹的代码块里的异常。那漏网之鱼怎么办?

这时候就需要window.onerror出马了。它就像一个全局的异常监听器,任何未被try-catch捕获的JS异常,都会触发它。

window.onerror = function(message, source, lineno, colno, error) {
    console.error('全局异常捕获:', {
        message: message,
        source: source,
        line: lineno,
        column: colno,
        stack: error ? error.stack : '无堆栈信息'
    });
    
    // 上报到日志系统
    reportGlobalError({
        message,
        source,
        lineno,
        colno,
        stack: error?.stack
    });
    
    // 返回true表示异常已被处理,浏览器不再打印默认错误
    return true;
};

重要提示:window.onerror有个限制——它捕获不到跨域脚本的详细错误信息。如果你引用了CDN上的脚本,记得加上crossorigin="anonymous"属性,否则你只能看到"Script error."这种毫无价值的信息。

嗯,这里要注意:window.onerror也捕获不到Promise里的异常。那Promise的异常怎么处理?往下看。

3.3 Promise异常处理:.catch()与unhandledrejection

Promise的出现让异步编程变得优雅,但也带来了新的异常处理问题。

最基本的做法是用.catch()捕获:

fetch('/api/data')
    .then(response => response.json())
    .then(data => {
        // 处理数据
        processData(data);
    })
    .catch(error => {
        // 捕获fetch或处理过程中的任何异常
        console.error('请求失败:', error);
    });

但问题来了——如果某个Promise忘了加.catch(),异常就会静默消失。你想想看,这多可怕?

所以浏览器提供了unhandledrejection事件,专门捕获那些没有被处理的Promise异常:

window.addEventListener('unhandledrejection', function(event) {
    console.error('未处理的Promise异常:', event.reason);
    
    // 上报异常
    reportUnhandledRejection(event.reason);
    
    // 阻止默认处理(浏览器控制台打印)
    event.preventDefault();
});

我的建议:在项目初始化时就注册unhandledrejection监听器,把它和window.onerror一起作为全局异常捕获的「双保险」。

3.4 async/await异常处理:try-catch的优雅回归

async/await让异步代码写起来像同步一样。异常处理也回归到了最熟悉的try-catch模式。

async function fetchUserData(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
            throw new Error(`HTTP错误: ${response.status}`);
        }
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('获取用户数据失败:', error);
        // 可以在这里做降级处理
        return getCachedUserData(userId);
    }
}

这里有个很多人容易忽略的点——await后面的Promise如果reject了,会被try-catch捕获到。但如果你忘了写try-catch,那这个异常就会变成unhandledrejection。

我曾经踩过的坑:在async函数里用了多个await,但只在外层包了一个try-catch。结果第一个await就抛异常了,后面的代码全没执行。后来我改成每个关键操作单独try-catch,或者用Promise.allSettled来处理。

3.5 知识体系总览

说了这么多,咱们用一张图来总结一下JS异常捕获的完整体系:

JavaScript异常捕获体系 JavaScript异常来源 同步异常 异步异常(Promise) 异步异常(async/await) try-catch-finally 局部捕获,精确控制 .catch() 链式捕获 try-catch(async) 同步风格捕获异步异常 全局兜底:window.onerror + unhandledrejection 捕获所有漏网之鱼 异常上报 + 日志采集

3.6 实战建议

说了这么多理论,最后给几条实战建议:

  • 分层捕获:不要只依赖全局捕获。每个关键操作都加上try-catch,精确控制异常处理逻辑。
  • 统一上报:所有捕获到的异常,最终都要上报到你的日志系统。我习惯封装一个reportError函数,统一处理。
  • 不要吞异常:catch里只console.error是不够的,一定要做点什么——要么上报,要么降级,要么重试。
  • 区分异常类型:网络错误、业务错误、代码错误,处理方式完全不同。建议用自定义错误类来区分。

我的个人习惯:在项目入口文件里同时注册window.onerror和unhandledrejection,然后在每个模块的关键函数里用try-catch包裹。这样既有全局兜底,又有局部精确控制,基本不会漏掉任何异常。

好了,JS异常捕获这块就聊到这儿。记住一句话:异常捕获不是为了让代码不出错,而是为了出错时我们能知道、能处理、能恢复


专注资料整理