静默故障:你必须避免的初级陷阱
Source: Dev.to

你一定有过这种经历。用户点击 Refund(退款)按钮。加载动画旋转。加载动画停止。
什么也没有发生。
- 屏幕上没有错误信息。
- 控制台里没有红色文字。
- 监控面板上没有警报。
用户又点击了五次。仍然没有任何反应。
你深入代码,寻找 bug。你层层追踪逻辑,穿过三层抽象,终于找到了它:
if (!success) return false;
这就是 Silent Failure——最难调试的错误类型,因为它在现场销毁了所有证据。
乐观 vs. 偏执
- Juniors 通常是 乐观 的。他们为理想路径编写代码——数据库总是能连接,订单总是存在,API 总是在 200 ms 内响应。他们返回
false或null,因为害怕让应用崩溃。 - Professionals 必须是 偏执 的。他们假设网络拥塞,数据库耗尽,且输入可能是恶意的。
今天,我将向你介绍 The Integrity Check ——一种将脆弱、乐观的逻辑转变为稳健、防御性工程的模式。
初级陷阱:乐观返回
让我们来看一个用于处理退款的函数。 “乐观的初级开发者” 编写的代码假设订单存在且状态有效。如果出现问题,他们返回 false 以保持应用“存活”。
// Before: The Optimistic Junior
// The Problem: Silent failures and no validation.
async function processRefund(orderId) {
const order = await db.getOrder(orderId);
// If the order doesn't exist... just return false?
// The caller has no idea WHY it failed. Was it the DB? The ID?
if (!order) {
return false;
}
// Business Logic mixed with control flow
if (order.status === 'completed') {
await bankApi.refund(order.amount);
return true;
}
// If status is 'pending', we fail silently again.
return false;
}
为什么这会让“缺乏睡眠的资深测试”失败
想象一下现在是凌晨 3 点。支持工单上写着 “退款无法工作”。 你查看日志——没有错误。再看代码——它只是返回了 false。
- 退款失败是因为 ID 错误吗?
- 是因为订单已经退款了吗?
- 还是因为银行 API 挂了?
你只能猜测。这是不可接受的。
专业操作:完整性检查
为了解决这个问题,我们需要将 Paranoia(偏执) 与 Loudness(响亮) 结合起来。
- Paranoia(偏执):我们不信任输入或状态,必须立即进行验证。
- Loudness(响亮):如果函数无法实现其名称所描述的功能,它应该大声报错(抛出异常)。
我们将使用 Guard Clauses(守卫语句) 和 Explicit Errors(显式错误) 进行重构。
转换示例
// After: The Professional Junior
// The Fix: Loud failures, defensive coding, and context.
async function processRefund(orderId) {
const order = await db.getOrder(orderId);
// 1. Guard Clause – stop execution if the data is missing.
if (!order) {
throw new Error(`Refund failed: Order ${orderId} not found.`);
}
// 2. State Validation – ensure the order is in a refundable state.
if (order.status !== 'completed') {
throw new Error(
`Refund failed: Order is ${order.status}, not completed.`
);
}
// 3. Handle External Chaos – wrap third‑party calls to add context.
try {
await bankApi.refund(order.amount);
} catch (error) {
// Add context so we know *where* it failed.
throw new Error(`Bank Gateway Error: ${error.message}`);
}
}
为什么这样更好
1. Guard Clause Pattern
我们颠倒 if 语句:不再在 if (success) { … } 中嵌套逻辑,而是先检查失败并立即返回。这使代码变得平坦,去掉缩进,便于扫描(Visual Geography)。
- Junior: 检查成功路径(
if (exists))。 - Pro: 检查失败路径(
if (!exists))。
2. Law of Loudness
在 After 示例中我们从不返回 false。
- 如果 ID 错误 →
Refund failed: Order 123 not found. - 如果订单处于待处理状态 →
Refund failed: Order is pending, not completed.
错误信息明确告诉我们如何修复 bug。我们无需调试代码,只需阅读日志。
3. Contextual Wrappers
第三方 API 通常抛出通用错误,例如 500 Server Error。如果让它直接向上抛出,我们就不知道错误来源是 User Service、Bank 还是 Emailer。
通过在 bankApi 调用外使用 try/catch 包装,我们在错误前加上上下文前缀:Bank Gateway Error: …。现在我们可以准确知道是哪一个集成出现问题。
The Takeaway
下次当你想在出错时写 return null 或 return false 时,先停下来。问问自己:
“如果这件事在凌晨 3 点发生,我能知道原因吗?”
如果答案是否,就抛出异常。能够大声且及时抱怨的代码,更容易维护。
保持偏执。保持响亮。保持专业。
停止编写悄悄失败的代码。
本文摘自我的手册 《专业初级:编写有意义的代码》。它不是一本 400 页的教材,而是一本关于未成文工程规则的实用现场指南。
👉 获取完整手册
