编程语言

JavaScript 错误处理实践:从 try/catch 到全局兜底与重试

写给中后端与全栈 JS 工程师的方法清单:同步/异步错误怎么接、Promise 的 unhandledrejection、兜底与指数退避重试怎么写。

作者:巧匠团队·7 分钟阅读·更新于 2026-09-06

同步与异步根本不在同一轨道:先分清两套 catch

try/catch 只能兜住同步抛错与 await 到的 rejected Promise。setTimeout 回调里的 throw、以及不 await 就被 reject 的 Promise,try/catch 接不住。

async 函数里你既能用 try/catch 包 await,也能把它当普通 Promise 返回交给外层 .catch。选择取决于你是就地恢复(try/catch)还是交给调用方(返回 promise)。

很多人踩的一个坑:把 try/catch 包住整个 await 序列,却不知道其中某个异步根本没被 await——那个错误会漏到 unhandledrejection。

// 异步里的 throw 不会被外层 try/catch 拦住:
try {
  setTimeout(() => { throw new Error("boom"); }, 0);
} catch (e) {
  console.log("caught");  // 永远不会到
}

// 正确接住异步错误:await 必须真的被 await
try {
  await risky();
} catch (e) {
  console.log("caught", e.message);
}

// 或者把 promise 交给调用方
function loadUser(id) {
  return fetchUser(id).then(normalize);
}
// loadUser(...).catch(...) by the caller

给出足够的上下文:别丢 err 的原貌

常见错误是把 err 只 log 成字符串 "something failed",丢失 stack 与 cause。真崩溃十分钟后谁都想看到堆栈,而不是一句没头没尾的话。

现代环境有 Error Cause 链(new Error("context", { cause: original })),把“这里失败”和“底层根因”串起来,排查嵌套依赖特别有用。

规范校验库抛错时,多数是抛新 Error,原错误被吞掉要小心:把原始 err 当作 cause 一并抛出,或至少 log 它,别为了格式化而销毁它。

async function fetchProfile() {
  try {
    const blob = await http.get("/me");
    return parseProfile(blob);
  } catch (err) {
    // 保留原始错误为 cause,外面可看堆栈
    throw new Error("failed to load profile", { cause: err });
  }
}

try {
  await fetchProfile();
} catch (err) {
  console.error(err.message);        // failed to load profile
  console.error(err.cause?.message); // 底层根因
  console.error(err.cause?.stack);
}

错误示范:吞掉错误只发一条空日志

有些 try/catch 的 catch 里只写 console.error("oops") 或干脆空着。现场报障时无堆栈、无上下文,无法复现。

更隐蔽的变体是 catch 里返回一个“假的成功”值(如 return null 但没人判空),下游一路空引用打穿。

修复的准则是:能恢复才 catch 并转成明确的失败信号,不能恢复就带完整 cause 的重新抛出,绝不让一个错误变成二十条无关的连带报错。

// 错误示范:吞掉并伪造成功
async function getScore() {
  try {
    return await fetchScore();
  } catch {
    return null;  // 下游 if (!score) throw 一通乱
  }
}

// 修复:区分“能恢复”与“直接暴露”
async function getScore() {
  try {
    return await fetchScore();
  } catch (err) {
    // 有限次数降级可恢复,否则带 cause 重新抛出
    logger.error({ err, msg: "fetchScore failed, falling back to 0" });
    return cachedScore() ?? 0;
  }
}

全局兜底:进程与浏览器事件把漏网错误收起来

无论机制多完善,总会有漏网之鱼走到 unhandledrejection / uncaughtException。全局挂钩能把它们记录到监控而不是静默消失,Node 上还能决定进程要不要退出。

Node 里 process.on("uncaughtException") 与 process.on("unhandledRejection") 收末端;注意 uncaughtException 兜底后进程状态可能已不可信,通常记日志、发告警、妥善退出并让容器/编排器重启。

浏览器端对应 window.onerror 与 unhandledrejection,配合 Sentry 或自建上报,能把用户侧吞掉的错误也收回来。

// Node 末端兜底
process.on("uncaughtException", (err) => {
  reportToMonitoring(err);
  console.error("FATAL", err);
  process.exit(1);          // 状态不可信,交给编排重启
});

process.on("unhandledRejection", (reason) => {
  reportToMonitoring(reason);
});

// 浏览器
window.addEventListener("error", (e) => reportToMonitoring(e.error));
window.addEventListener("unhandledrejection", (e) =>
  reportToMonitoring(e.reason),
);

重试与退避:短暂故障不是失败

网络抖动、下游 5xx、连接池打满通常都是瞬时。恰当的重试能把“偶发失败”抹平,但盲目无限重试会把瞬时风暴放大成雪崩。

指数退避加上随机抖动(jitter)避免多客户端同步重试撞车;设置 totalTimeout 或 maxAttempts 兜底;只在幂等或可安全重试的请求上重试(POST 要评估)。

把重试策略放到通用 HTTP 封装里,而不是每个调用处各写一套,才能保证一致性、可观测(记录每次重试原因)与可选降级。

async function withRetry(fn, { maxAttempts = 4, base = 200 } = {}) {
  let lastErr;
  for (let i = 0; i < maxAttempts; i++) {
    try {
      return await fn();
    } catch (err) {
      lastErr = err;
      const shouldStop = err.retryable === false || i === maxAttempts - 1;
      if (shouldStop) break;
      const delay = base * 2 ** i + Math.floor(Math.random() * base); // jitter
      console.warn(`attempt ${i + 1} failed, retry in ${delay}ms`, err.message);
      await new Promise((r) => setTimeout(r, delay));
    }
  }
  throw lastErr;
}

const data = await withRetry(() => http.get("/api/orders"));

官方参考来源

下方为命令对应的官方权威文档,供你核对最新用法与深入查阅。