学完这篇,你能分清哪些错误该用 try-catch 就地处理、哪些交给全局做最后的保底捕获,并写出一套不会吞掉错误、能定位问题的处理代码。
第一步:先分清 try-catch 能抓到什么
try-catch 只能捕获同步执行过程中抛出的异常。它抓不到三类错误:语法错误(脚本解析阶段就失败,代码根本没跑起来)、setTimeout/事件回调里异步抛出的错误(调用栈已经回到事件循环)、以及 Promise 被拒绝(rejection)。
注意:在 try 块里写 setTimeout(() => { throw new Error('x') }) 是抓不到的。这个异常会直接冒到全局,别指望外层 try 能保底。
第二步:只给可能失败的代码加 try-catch
最常见的坏习惯是把整个函数体塞进 try,出错后完全不知道是哪一行。正确做法是只包住少量语句,并且在 catch 里给出明确的处理方式。
function readConfig(raw) {
try {
return JSON.parse(raw);
} catch (err) {
console.warn('配置解析失败,回退默认值', err);
return { theme: 'default' };
}
}
核心原则:能预期、能降级的错误就地处理;处理不了的往上抛。catch 里什么都不做是给以后留下隐患,至少记一条日志。
第三步:async 函数与 Promise 的捕获方式
async 函数里的 await 抛错,可以被同一函数体内的 try-catch 捕获,这是最推荐的写法:
async function loadList() {
try {
const res = await fetch('/api/list');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error('加载列表失败', err);
return [];
}
}
如果用的是 .then() 链式写法,就必须在末尾补 .catch();await 后面不要漏掉 throw 检查 res.ok,因为 fetch 只有网络层失败才会 reject,404、500 都不会。
第四步:全局捕获作为最后的保底
页面总会有漏网之鱼,需要兜底上报:
window.addEventListener('error', (e) => {
// 资源加载失败时 e.target 是元素,不是 window
if (e.target && e.target !== window) {
report({ type: 'resource', url: e.target.src || e.target.href });
return;
}
report(e.error || new Error(e.message), { file: e.filename, line: e.lineno });
}, true); // 第三个参数 true 才能捕获资源错误
window.addEventListener('unhandledrejection', (e) => {
report(e.reason instanceof Error ? e.reason : new Error(String(e.reason)));
});
注意:资源错误(<img>、<script> 的 404)不冒泡,必须在捕获阶段监听才拿得到。另外 window.onerror 一次只能挂一个,用 addEventListener 才能挂多个监听器。
第五步:自定义错误类型,保留原始原因
抛出错误时不要丢了原始上下文,用 cause 把底层错误链接起来,用自定义类区分错误类型:
class ApiError extends Error {
constructor(message, { status, cause } = {}) {
super(message, { cause });
this.name = 'ApiError';
this.status = status;
}
}
try {
await fetch('/api/save', { method: 'POST' });
} catch (err) {
throw new ApiError('保存失败', { status: 500, cause: err });
}
调用方就能按 err instanceof ApiError 走不同的处理分支,而不是靠匹配错误信息字符串。
第六步:用 finally 保证清理
释放连接、关闭 loading、销毁定时器这类动作,一律放进 finally,保证成功失败都会执行:
loading.show();
try {
await submit();
} finally {
loading.hide();
}
常见问题
- 跨域脚本报错信息变成
Script error.:给引入的脚本加 crossorigin="anonymous",并且 CDN 返回 Access-Control-Allow-Origin。
- 上报接口本身出错导致死循环:上报函数内部要包一层 try,失败就静默丢弃。
- 错误频繁上报:同一 message 加时间窗限流(比如 5 秒内只报一次)。
- 全局监听里加
e.preventDefault():会阻止控制台默认输出,只在确实要完全接管时才用。
小结
try-catch 只抓同步异常,异步、Promise、语法错误都抓不到。
try 块要小,catch 要有明确处理或日志,不吞错。
async/await 优先用 try-catch;.then 链末尾必须有 .catch。
window.addEventListener('error', fn, true) 是唯一的全局兜底,第三个参数不能省略;unhandledrejection 单独监听。
- 错误对象用自定义类 +
cause 保留链路,日志带文件名与行号才可定位。
- 用
finally 做资源清理,上报前做限流防风暴。