场景一:try-catch 捕获不到 Promise.reject?
js
// ❌ 线上出现 Unhandled Rejection,但明明写了 try-catch
async function loadConfig() {
try {
const config = await fetchConfig();
return config;
} catch (e) {
console.error('配置加载失败', e);
return fallbackConfig;
}
}
loadConfig();
结果:生产环境依然上报 UnhandledPromiseRejection。
原因分析
问题不在 loadConfig 内部,而在于调用方没有 await 。async 函数返回的 Promise 如果被"悬空"调用,内部 catch 虽然执行了,但外部引用这个 Promise 时若再次 reject,就会产生未捕获的 rejection。
实际场景中 fetchConfig 抛出了一个非 Error 类型的值 (比如数字 403),catch 捕获到后 console.error 打印了它,但紧接着又执行了 return fallbackConfig,整个过程 Promise 是 resolved 的。但如果 fetchConfig 在 Promise 中抛出的值被异步事件循环忽略了呢?------ 这是另一个陷阱。
解决方案
始终在顶层调用处处理 Promise:
js
// ✅ 在调用链末端加 catch 兜底
loadConfig().catch(err => {
console.error('未捕获的配置加载异常', err);
});
或者给 Node.js/浏览器挂载全局兜底:
js
// 浏览器
window.addEventListener('unhandledrejection', event => {
console.error('未捕获的 Promise 拒绝:', event.reason);
event.preventDefault(); // 阻止默认打印
});
// Node.js
process.on('unhandledRejection', (reason, promise) => {
console.error('未捕获的 Promise 拒绝:', reason);
});
场景二:forEach 中的 await 吞掉了所有错误
js
// ❌ 只会捕获到第一个错误,后面的请求全都"静默失败"
async function batchFetch(urls) {
try {
urls.forEach(async (url) => {
const data = await fetch(url);
processData(data);
});
} catch (e) {
console.error('批量请求失败', e);
}
}
原因分析
forEach 的回调是独立的 async 函数 ,每个回调返回的 Promise 没有人 await。外部 try-catch 只能捕获同步代码抛出的异常,而对那些返回的 Promise 完全"视而不见"。
假设 5 个 URL 中第 2 个请求 401 了:
- 第 1 个正常执行 ✅
- 第 2 个走到
catch分支 - 第 3、4、5 个继续执行(独立 Promise 互不影响)
- 但外层 try-catch 根本没有捕获到任何异常
这就是线上经常出现的"部分请求失败但监控不到"的经典原因。
解决方案
js
// ✅ 方案一:for...of + await(串行)
async function batchFetchSequential(urls) {
for (const url of urls) {
try {
const data = await fetch(url);
processData(data);
} catch (e) {
console.error(`请求失败: ${url}`, e);
}
}
}
// ✅ 方案二:Promise.allSettled(并行,不中断)
async function batchFetchAllSettled(urls) {
const results = await Promise.allSettled(
urls.map(url => fetch(url).then(data => processData(data)))
);
const errors = results.filter(r => r.status === 'rejected');
if (errors.length > 0) {
console.error(`${errors.length} 个请求失败`, errors.map(e => e.reason));
}
}
场景三:全局错误拦截被 async 函数绕过
js
// ❌ window.onerror 在 async await 下失效
window.onerror = function(msg, url, line, col, error) {
console.log('全局错误:', msg);
return true;
};
async function main() {
throw new Error('async error');
}
main();
原因分析
window.onerror 只能捕获同步代码 中的运行时错误。async 函数返回的 Promise 被 reject 时,并不会触发 onerror,而是触发 unhandledrejection 事件。
解决方案
js
// ✅ 全局错误监控的正确姿势
window.addEventListener('error', event => {
// 捕获资源加载错误 + 同步 JS 错误
console.error('全局错误:', event.error || event.message);
});
window.addEventListener('unhandledrejection', event => {
// 捕获 async/await 和 Promise 的未处理拒绝
console.error('未处理的 Promise 拒绝:', event.reason);
});
要点 :
onerror管同步,unhandledrejection管异步。线上监控两者都须挂载。
场景四:JSON.parse 异常悄悄被 async 吞掉
js
// ❌ 线上排查了很久才发现是 JSON 解析异常被吞了
async function fetchAndParse(url) {
try {
const res = await fetch(url);
const text = await res.text(); // 假设接口返回了 502 HTML 页面
return JSON.parse(text); // ❌ 此时 JSON.parse 抛异常
} catch (e) {
// 注意:e 是字符串 '"Unexpected token < in JSON at position 0"'
// 日志里只有 "JSON.parse 失败",没有原始响应内容
console.error('JSON.parse 失败:', e.message);
return null;
}
}
原因分析
这个写法的实际问题是:当 JSON.parse 失败时,catch 确实捕获到了异常。但如果你在 catch 里没有记录上下文 (比如原始响应文本 text),排查时就只能看到"JSON.parse 失败",根本不知道接口返回了什么。
更隐蔽的问题:如果 JSON.parse 抛出的是非 Error 类型 (例如在某些 polyfill 或旧浏览器中),e.message 可能是 undefined,导致日志里连错误信息都没有。
解决方案
js
// ✅ 方案一:细分 try-catch 并记录上下文
async function fetchAndParse(url) {
let res, text;
try {
res = await fetch(url);
text = await res.text();
return JSON.parse(text);
} catch (e) {
console.error('请求/解析失败:', {
url,
status: res?.status,
text: text?.slice(0, 500), // 保存原始响应的前 500 字符
error: e instanceof Error ? e.message : String(e)
});
return null;
}
}
// ✅ 方案二:用 async/await 包裹 JSON.parse 单独 try
async function safeJsonParse(str) {
try {
return { ok: true, data: JSON.parse(str) };
} catch (e) {
return { ok: false, error: e, raw: str };
}
}
场景五:catch 分支误以为一定会收到 Error 对象
js
// ❌ 线上日志出现大量 "[object Object]" 堆栈缺失
async function risky() {
// 模拟:某个 SDK 抛出的不是 Error
throw { code: 403, message: 'Forbidden', details: { userId: 123 } };
}
try {
await risky();
} catch (e) {
// e 不是 Error 对象
console.error('错误详情:', e); // { code: 403, message: 'Forbidden', ... }
console.error(e.stack); // ❌ undefined
console.error(e.message); // ❌ undefined(注意不是 'Forbidden')
sendToErrorMonitor(e); // ❌ 监控平台收不到有效 stack
}
原因分析
很多开发者的潜意识和 TypeScript 类型注解都暗示 catch 的形参一定是 Error 类型。但 JavaScript 中 throw 可以抛出任意类型 的值:对象、字符串、数字、甚至 undefined。e.stack、e.message 在这些情况下全部是 undefined。
线上场景中:
- 部分第三方 SDK 用
reject({ code, message })抛出对象 - WebSocket 错误回调传的是
(event),不是 Error - 某些浏览器跨域脚本错误会抛
Script error.(字符串)
解决方案
js
// ✅ 统一错误格式化
function normalizeError(err) {
if (err instanceof Error) return err;
// 对象类型:提取关键信息
if (typeof err === 'object' && err !== null) {
const msg = err.message || err.msg || err.code || '';
const normalized = new Error(String(msg));
normalized.original = err;
return normalized;
}
// 字符串/数字/其他类型
return new Error(String(err));
}
// 使用方式
try {
await risky();
} catch (e) {
const normalized = normalizeError(e);
console.error('原始错误:', e);
console.error('规范化错误:', normalized.message);
console.error(normalized.stack); // ✅ 一定有 stack
sendToErrorMonitor(normalized);
}
场景六:finally 中 return 吞掉了 catch 的异常
js
// ❌ 异常被静默吞掉,catch 形同虚设
async function process() {
try {
await doRiskyWork();
} catch (e) {
console.error('捕获到异常:', e);
throw e; // 重新抛出,期望调用方处理
} finally {
return 'fallback'; // ❌ finally 中的 return 覆盖了 throw
}
}
const result = await process();
console.log(result); // 'fallback' --- 异常被悄无声息地吞掉了
原因分析
这是一个鲜为人知的 JavaScript 语言特性:无论在 try 中 return 还是在 catch 中 throw,最终函数的返回值都会被 finally 块中的 return 覆盖。这个行为在同步代码和 async 函数中同样生效。
线上场景:某个资源释放逻辑写在 finally 中,某天有人在 finally 末尾加了个 return 兜底返回值,结果所有异常都被吞了,调用方永远收不到错误信号。
解决方案
js
// ✅ 方案一:finally 中不要 return
async function process() {
try {
return await doRiskyWork();
} catch (e) {
console.error('捕获到异常:', e);
throw e;
} finally {
// ✅ 只做清理,不要 return
await releaseResources();
// 没有 return!
}
}
// ✅ 方案二:如果确实需要 finally 返回值,明确语义
async function process() {
let result;
try {
result = await doRiskyWork();
} catch (e) {
console.error('捕获到异常:', e);
result = 'errorFallback';
} finally {
await releaseResources();
}
return result; // 在 finally 之外 return
}
要点总结
| # | 陷阱 | 一句话修复 |
|---|---|---|
| 1 | async 函数外层未 await,catch 形同虚设 | 调用链末端挂 .catch() 或全局挂 unhandledrejection |
| 2 | forEach + await 吞异常 |
改用 for...of 或 Promise.allSettled |
| 3 | window.onerror 抓不到 async 错误 |
同时监听 unhandledrejection 事件 |
| 4 | try-catch 中丢失上下文信息 | 在 catch 中记录原始响应及相关状态码 |
| 5 | catch 假设 e 是 Error 对象 | 统一 normalizeError 函数规范化异常 |
| 6 | finally 中 return 覆盖异常 | finally 块中永远不要 return |
核心原则:
- 始终在调用链最外层兜底,不要依赖"内部 catch 就能覆盖所有场景"
- 所有异常进入监控系统之前必须规范化为 Error 对象
- 记录异常时附带上尽可能多的上下文(URL、状态码、原始响应)
这六类问题我曾在不同项目线上排查中全部都遇到过,尤其是第 2 条 forEach 和第 6 条 finally,排查耗时最长,最后发现是语言层面的"反直觉"行为。希望你看完不用再踩同样的坑。