同步与异步根本不在同一轨道:先分清两套 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"));