JS 代码技巧 vol.5 — 10 个错误处理套路,从 try/catch 到 Error Boundary
小不的代码技巧系列第 5 期
主题:错误处理套路
这期话题有点重——错误处理这事,写好是"稳如老狗",写差是"半夜被告警"😈
哈喽哇!我是小不,不简说的不~
作为一个在代码界"翻车"无数次的选手,我算是看明白了:坑这东西吧,要么不踩,踩就踩大的😂
今天这期,主打一个 错误处理套路——不整虚的,全是实战里哭出来的教训。看不看随你~反正翻车实录又不收钱
Ps:文末有惊喜(不是广告!)
1. async/await 时代:try/catch 统一收口
Promise 链时代大家都用 .catch,async/await 出来后,try/catch 是首选。
// ❌ 反例:混用风格,心智负担重
fetchUser(id)
.then((user) => render(user))
.catch((e) => console.error(e));
// ✅ 正解:async/await + try/catch
async function loadUser(id) {
try {
const user = await fetchUser(id);
render(user);
} catch (e) {
console.error("加载用户失败:", e);
}
}
坑在哪:async 函数里 throw 出去,调用方不 try/catch 也不 .catch,就是 unhandled rejection。Node 15+ 默认让进程退出,浏览器打 warning 还得靠 unhandledrejection 监听。
2. try/catch 不能跨 setTimeout 边界
经典踩坑:
// ❌ 反例:以为能 catch 到
try {
setTimeout(() => {
throw new Error("boom");
}, 100);
} catch (e) {
console.error("抓到了"); // ❌ 根本抓不到
}
正解——错误必须自己处理:
setTimeout(() => {
try {
doSomething();
} catch (e) {
console.error("抓到了", e); // ✓
}
}, 100);
坑在哪:定时器、Promise.then、事件回调里的 throw,都不会冒泡到外层 try/catch。每写一个异步回调都要想"我里面的错误会跑去哪"——这是异步错误处理的灵魂拷问。
3. 顶层 unhandledrejection 兜底:必须有
// 浏览器
window.addEventListener("unhandledrejection", (e) => {
console.error("未捕获的 Promise 拒绝:", e.reason);
e.preventDefault(); // 阻止默认的 console 输出
// 上报到 Sentry / 自家监控系统
});
// Node
process.on("unhandledRejection", (reason, promise) => {
console.error("未捕获的 Promise 拒绝:", reason);
// 业务上报
});
坑在哪:光监听不上报等于没写。生产环境必须把 e.reason 里的 stack 抓到监控系统,不然线上炸了都不知道炸在哪。没有监控的错误处理都是自欺欺人。
4. Error 子类化:自定义错误类型
业务里混用普通 Error 等于"错误全是一坨",定位起来血压升高。
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
class NetworkError extends Error {
constructor(message, status) {
super(message);
this.name = "NetworkError";
this.status = status;
}
}
// 用法
function saveUser(data) {
if (!data.name) throw new ValidationError("用户名必填", "name");
}
坑在哪:extends Error 在 TS 编译到 ES5 时有坑(target 调高就行)。new Error().name 记得改,否则全是 "Error" 一锅炖。Sentry 这类工具也是靠 name 字段分类的,不改等于裸奔。
5. 错误边界(Error Boundary):不让一个组件挂掉整个 app
类 React 框架都有 Error Boundary,Vue 里有 errorCaptured,原生 JS 自己写:
// 简易版:保护一块 UI 不被单个组件错误带崩
function safeRender(fn, fallback) {
try {
return fn();
} catch (e) {
console.error("组件渲染失败:", e);
return fallback;
}
}
// 套娃用法
function App() {
return (
safeRender(
() => Header(),
() => "<div>Header 加载失败</div>",
) +
safeRender(
() => Main(),
() => "<div>Main 加载失败</div>",
)
);
}
坑在哪:错误边界只挡"同步 throw",异步错误(fetch、setTimeout)还是要靠 try/catch 自己处理。错误边界不是万能药,异步错误永远在边界外。
6. throw vs return Error:业务层选择
两种风格,看场景:
// 风格 A:throw(异常流)
function getUser(id) {
const user = db.find(id);
if (!user) throw new NotFoundError("用户不存在");
return user;
}
// 风格 B:return null/result(控制流)
function getUser(id) {
const user = db.find(id);
return user ?? null; // 调用方自己判断
}
坑在哪:业务上"找不到用户"是异常还是正常?用 throw 当控制流是反模式——error stack 创建有成本,正常分支就别 throw。区分编程错误(throw:传了 null 进来这种程序员失误)和业务错误(return:用户没找到这种正常情况)。
7. catch 里"吞"错误是万恶之源
// ❌ 经典反模式
try {
doSomething();
} catch (e) {
// 啥也不干
}
至少要这样:
try {
doSomething();
} catch (e) {
console.error(e); // 至少留个痕迹
// 或者上报 Sentry
// 或者把错误往上传给调用方
}
坑在哪:"吞错"最怕的是默默积累。所有 catch 都得留下痕迹!所有 catch 都得留下痕迹!所有 catch 都得留下痕迹! ⚠️ 等用户报 bug 时啥日志都没有,才知道当初那个"懒得写 console.error"有多致命。
8. Error.captureStackTrace:自定义错误保留干净堆栈
V8 提供的 API,让你的自定义错误从"被抛出的地方"开始记录堆栈,而不是从构造函数:
class BizError extends Error {
constructor(message) {
super(message);
this.name = "BizError";
Error.captureStackTrace(this, BizError); // 第二个参数:从这里开始截断
}
}
throw new BizError("业务出错");
// 堆栈会跳过 BizError 构造函数那层,直接到 throw 行
坑在哪:这是 V8 专属 API(Node 和 Chrome 都有),Firefox 没有——跨运行时小心。简单的项目直接 super(message) 也行,别为了漂亮堆栈费这个劲。写给自己看 vs 写给团队看,自己拿捏。
9. 进程级兜底:uncaughtException + unhandledRejection
Node 服务必装:
process.on("uncaughtException", (err) => {
console.error("未捕获异常:", err);
// 上报
// 视情况决定是否退出(推荐:退出让 pm2/systemd 重启)
process.exit(1);
});
process.on("unhandledRejection", (reason) => {
console.error("未捕获 Promise 拒绝:", reason);
process.exit(1);
});
坑在哪:这两个事件只用于兜底和上报。别在这里写业务逻辑!别在这里写业务逻辑!别在这里写业务逻辑! ⚠️ 进程状态已经不可信,DB 连接可能断了,Redis 可能挂了一半,写啥都可能是错上加错。记录 → 上报 → 退出 → 让守护进程重启,是标准流程。
10. 可选链 + 空值合并 vs try/catch:防御式 vs 异常式
两种错误处理风格:
// 防御式(Go 风格)
const name = user?.profile?.name ?? "匿名";
const age = user?.profile?.age ?? 0;
// 不抛错,靠默认值兜底
// 异常式(Java/Rust 风格)
try {
const user = await fetchUser(id);
render(user.profile.name);
} catch (e) {
// 集中处理错误
}
坑在哪:混用是大忌。函数 A 用 return null,函数 B 用 throw,调用方一脸懵。团队先定调:
- 轻业务(表单、UI 状态):防御式优先,少 try/catch
- 重业务(支付、订单):异常式优先,关键路径必 try/catch + 上报
📦 收个尾
这 10 个技巧浓缩一下:
- 最高频踩坑:try/catch 跨 setTimeout 边界、catch 里"吞"错、process 兜底写业务
- 最值得收藏:Error 子类化、unhandledrejection 监听、
Error.captureStackTrace - 核心思路:区分编程错误(throw)和业务错误(return)
学到了就是赚到了,犹豫徘徊等于白来~
写到最后
想要啥技巧?评论区甩个题目过来~
- 你刚踩的坑
- 项目里反复写的代码
- 想搞清楚但一直懒得查的 API
小不看到…不一定回 😂 毕竟代码里翻车太多,腾不出手~
Ps:三连随缘,催更的会被打 😂