写过前端的人几乎都遇到过这种情况:本地跑得好好的页面,一到线上就莫名白屏,打开控制台才发现一串红色报错。异常处理看起来简单,无非是try catch包一下,但真正做好它并不容易——什么时候该捕获、捕获之后怎么处理、线上报错怎么定位到源码,每一环都有讲究。这篇文章就从基础语法讲到全局捕获,再到实际调试技巧,把JavaScript异常处理这条链路完整过一遍。

try catch finally 的标准用法与常见误区
try catch是最基础的异常捕获手段,但很多人对它的理解只停留在表面。先看标准写法:
function parseConfig(jsonString) {
try {
const config = JSON.parse(jsonString);
return config;
} catch (error) {
console.error('配置解析失败:', error.message);
return {};
} finally {
console.log('解析流程结束');
}
}这里有几个细节值得注意。第一,finally块无论是否抛出异常都会执行,即使try或catch里有return语句,finally的代码也会在返回之前跑完。一个经典面试题就是在finally里写return,它会覆盖try里的返回值,实际开发中千万别这么干。
第二,try catch只能捕获同步代码的异常。异步代码中的错误它无能为力,比如setTimeout回调里抛出的错误,外层的try catch是接不住的。这是最常见的一个误区,很多开发者以为把异步调用包在try里就万事大吉了,实际上错误早在事件循环的另一轮中抛出,try的作用域早已结束。
第三,catch里捕获的error对象包含几个重要属性:message是错误信息,name是错误类型名,stack是调用堆栈。排查问题时stack最值钱,它直接告诉你错误发生在哪一行、调用链是什么样的。
搞清楚六种内置错误类型,报错一看就懂
JavaScript内置了几种错误类型,看懂类型名基本就能判断问题出在哪。下面这张表整理了最常见的几种:
| 错误类型 | 触发场景 |
|---|---|
| TypeError | 对undefined取属性、调用不是函数的东西 |
| ReferenceError | 访问未声明的变量 |
| RangeError | 数值超出有效范围,如递归爆栈 |
| SyntaxError | 代码语法错误,或JSON.parse传入非法字符串 |
| URIError | encodeURI、decodeURI参数非法 |
| EvalError | eval使用不规范(现已少见) |
TypeError大概是日常遇到最多的一种,典型的报错信息是Cannot read properties of undefined,意思是在某个undefined的值上读取属性。定位方法很直接:看报错点号前面的变量,说明它在这个时刻是undefined,再沿着代码往上找它为什么没被赋值。
实际项目中还可以自定义错误类型,方便更精细的错误分类处理:
class BusinessError extends Error {
constructor(code, message) {
super(message);
this.name = 'BusinessError';
this.code = code;
}
}
try {
throw new BusinessError(40101, '登录已过期');
} catch (e) {
if (e instanceof BusinessError) {
// 业务错误,走统一的用户提示流程
showToast(e.message);
} else {
// 未知错误,直接上报
reportError(e);
}
}自定义错误的好处是把业务异常和程序异常区分开。业务异常比如参数校验不通过、权限不足,是预期内的,应该给用户友好提示;而TypeError这类程序异常往往意味着代码有bug,应该上报给监控系统而不是直接把堆栈甩给用户看。
全局错误捕获:兜住漏网之鱼
try catch没法包住所有代码,总有一些异常会漏掉。这时候需要全局捕获机制。浏览器提供了两个关键入口:
// 捕获同步运行时错误和资源加载错误
window.addEventListener('error', function (event) {
if (event.target && event.target !== window) {
// 资源加载失败,比如图片、脚本404
console.log('资源加载失败:', event.target.src || event.target.href);
} else {
// JS运行时错误
reportError({
message: event.message,
filename: event.filename,
lineno: event.lineno,
stack: event.error && event.error.stack
});
}
return true; // 阻止默认的控台输出(可选)
}, true);
// 捕获未处理的Promise rejection
window.addEventListener('unhandledrejection', function (event) {
reportError({
type: 'unhandledrejection',
reason: event.reason
});
});这里有个容易忽略的点:window的error事件要使用捕获阶段监听(第三个参数传true),因为资源加载错误不会冒泡,只能在捕获阶段拿到。判断是资源错误还是脚本错误,看event.target是不是window本身就行。
Promise的报错尤其要小心。一个Promise被reject了但没有对应的catch处理,控制台会报Uncaught (in promise)错误,这种错误在旧版浏览器里甚至不会中断后续代码,容易被忽视。线上有条件的话,建议把unhandledrejection捕获的错误也统一上报,很多隐蔽的接口异常就是这么被发现的。
高效调试技巧:从断点到线上排障
遇到问题别急着到处加console.log,浏览器的断点调试效率高得多。在Sources面板找到对应文件,点击行号就能打断点,代码执行到那里会自动暂停,此时可以把鼠标悬停在任意变量上查看值,也可以在右侧的Scope面板看完整的作用域链。条件断点特别实用:右键点击行号,输入一个表达式,只有表达式为true时才会中断,排查循环中某一次迭代的bug时能省大量时间。
还有一个技巧是debugger语句,直接写在代码里:
function calculateTotal(items) {
const total = items.reduce((sum, item) => {
debugger; // 打开开发者工具时,执行到这里会自动暂停
return sum + item.price * item.quantity;
}, 0);
return total;
}注意debugger只在开发者工具打开时生效,但要养成提交代码前检查的习惯,别把它带上线。
说到线上排障,最大的障碍是代码经过了压缩混淆,报错堆栈里的行号列号对不上源码。解决办法是source map:构建时生成.map文件(比如Webpack里的devtool配置),部署时map文件不上传到线上服务器,但接入错误监控平台如Sentry时上传给它。这样线上报错的堆栈就能自动还原成源码位置,直接定位到具体的源文件和行号。
把异常处理变成体系化的工程实践
单点技巧掌握了,更重要的是串成体系。一个成熟的异常处理方案通常分三层:最内层是局部try catch,只包住真正可能失败且有能力恢复的代码,比如JSON解析、接口调用;中间层是模块级的错误边界,比如React里的ErrorBoundary组件,防止局部错误导致整个页面崩溃;最外层是全局捕获加上监控上报,作为最后的兜底。
上报错误时建议带上足够的上下文:用户ID、页面URL、最近的操作记录、浏览器和设备信息,还有完整的堆栈。裸报一个错误信息价值有限,有了上下文才能复现问题。另外记得对上报做采样和去重,否则一个高频报错可能把上报接口打出雪崩。
最后提醒一点:异常处理不是把错误藏起来。catch块里只写一行空的处理是埋雷行为,问题依然存在,只是看不见了。正确的姿势是:能恢复的恢复,能降级的降级,不能处理的就上报并给出用户可感知的反馈。错误被妥善对待,代码的健壮性自然就上来了。
JavaScript异常处理try catchJS调试技巧修改时间:2026-09-05 07:08:37