静默故障:你必须避免的初级陷阱

发布: (2026年1月16日 GMT+8 19:12)
6 min read
原文: Dev.to

Source: Dev.to

《Silent Failures:你需要避免的初级陷阱》封面图片

Doogal Simpson

你一定有过这种经历。用户点击 Refund(退款)按钮。加载动画旋转。加载动画停止。

什么也没有发生。

  • 屏幕上没有错误信息。
  • 控制台里没有红色文字。
  • 监控面板上没有警报。

用户又点击了五次。仍然没有任何反应。

你深入代码,寻找 bug。你层层追踪逻辑,穿过三层抽象,终于找到了它:

if (!success) return false;

这就是 Silent Failure——最难调试的错误类型,因为它在现场销毁了所有证据。

乐观 vs. 偏执

  • Juniors 通常是 乐观 的。他们为理想路径编写代码——数据库总是能连接,订单总是存在,API 总是在 200 ms 内响应。他们返回 falsenull,因为害怕让应用崩溃。
  • 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 nullreturn false 时,先停下来。问问自己:

“如果这件事在凌晨 3 点发生,我能知道原因吗?”

如果答案是,就抛出异常。能够大声且及时抱怨的代码,更容易维护。

保持偏执。保持响亮。保持专业。
停止编写悄悄失败的代码。

本文摘自我的手册 《专业初级:编写有意义的代码》。它不是一本 400 页的教材,而是一本关于未成文工程规则的实用现场指南。

👉 获取完整手册

Back to Blog

相关文章

阅读更多 »