异步错误捕获的六大陷阱:await 裹着 try-catch 就一定稳了吗?

场景一: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 内部,而在于调用方没有 awaitasync 函数返回的 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 可以抛出任意类型 的值:对象、字符串、数字、甚至 undefinede.stacke.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...ofPromise.allSettled
3 window.onerror 抓不到 async 错误 同时监听 unhandledrejection 事件
4 try-catch 中丢失上下文信息 在 catch 中记录原始响应及相关状态码
5 catch 假设 e 是 Error 对象 统一 normalizeError 函数规范化异常
6 finally 中 return 覆盖异常 finally 块中永远不要 return

核心原则:

  1. 始终在调用链最外层兜底,不要依赖"内部 catch 就能覆盖所有场景"
  2. 所有异常进入监控系统之前必须规范化为 Error 对象
  3. 记录异常时附带上尽可能多的上下文(URL、状态码、原始响应)

这六类问题我曾在不同项目线上排查中全部都遇到过,尤其是第 2 条 forEach 和第 6 条 finally,排查耗时最长,最后发现是语言层面的"反直觉"行为。希望你看完不用再踩同样的坑。

相关推荐
SEO_juper7 分钟前
2026年用Python分析网站访问日志:看清Googlebot和AI爬虫怎么爬你的站(附完整代码)
开发语言·前端·seo·独立站·谷歌优化
摸鱼仙人~8 分钟前
React 原理进阶:彻底理解 re-render、React.memo、useMemo 与 useCallback
前端·vue.js
IMPYLH12 分钟前
HTML 的 <small> 元素
前端·网络·html
IT_陈寒13 分钟前
Vue的响应式让我加班到凌晨,问题竟出在这个不起眼的地方
前端·人工智能·后端
AlienZHOU9 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc12 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师13 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen14 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒14 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow15 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端