在30分钟内找出所有错误的调试框架

发布: (2026年2月20日 GMT+8 06:16)
9 分钟阅读
原文: Dev.to

Source: Dev.to

框架:REDUCE

R复制 → E检查 → D划分 → U理解 → C改变 → E评估

步骤 1:复现(最多 5 分钟)

如果你无法复现它,就无法修复它。就这么简单。

目标: 找到触发 bug 的 最小步骤集合
不是 “用户在做某事,然后就崩溃了”。

1. Create a new user with email "test@test.com"
2. Add item to cart
3. Click checkout
4. Enter the same email "test@test.com" in billing
5. BUG: "Account already exists" error on payment page

如果无法在本地复现

检查环境差异。90% 的“无法复现” bug 由以下原因导致:

  • 不同的数据(生产环境有本地没有的边缘案例)
  • 不同的时机(需要并发用户的竞争条件)
  • 不同的配置(环境变量、功能开关、软件包版本)
  • 缓存(本地缓存为空,生产缓存有过期数据)

给此步骤设定时间盒。 如果在 5 分钟内无法复现,请在进一步尝试之前收集更多数据(日志、截图、用户会话)。

步骤 2:检查(3 分钟)

在动代码之前,先收集证据。你知道(而不是怀疑)什么?

FACTS:
- Error: "Account already exists" on POST /api/checkout
- Happens when billing email matches an existing user
- Started after deploy v2.3.1 (didn’t happen in v2.3.0)
- Error comes from UserService.findOrCreate()
- Only happens with email‑based lookup, not with userId

UNKNOWNS:
- Why does checkout call findOrCreate? (It shouldn't create users)
- What changed between v2.3.0 and v2.3.1?
- Is this related to the auth refactor merged last week?

把这些写下来——在纸上、笔记里、注释中——任何实体的地方。这样可以防止你在原地打转。

第3步:划分(关键步骤)

这是决定成败的一步。 与其阅读所有代码,不如对 bug 进行二分搜索。

请求流是:Router → Middleware → Controller → Service → Repository → Database

问题: bug 在前半段还是后半段?

// Add a log at the midpoint: the service layer
async checkout(data: CheckoutData) {
  console.log('SERVICE INPUT:', JSON.stringify(data));
  const user = await this.userService.findOrCreate(data.email);
  console.log('SERVICE USER:', JSON.stringify(user));
  // ... rest of checkout
}

运行复现步骤并检查日志。

  • 如果 SERVICE INPUT 的数据错误 → bug 在前半段(router/middleware/controller)。
  • 如果 SERVICE INPUT 正确但 SERVICE USER 错误 → bug 在 findOrCreate
  • 如果两者都正确 → bug 在后半段。

现在你已经把搜索空间减半。重复此过程。每次划分只需 1‑2 分钟。经过 4‑5 次划分,你就能把范围从整个代码库缩小到单个函数。

这就是 git bisect,但针对代码路径而不是提交。

第4步:理解(不要只修复表面症状)

你已经找到了有 bug 的函数。在修改它之前,先弄清楚 它为什么会错

// The bug:
async findOrCreate(email: string): Promise {
  const user = await this.db.users.findOne({ email });
  if (!user) {
    return this.db.users.create({ email });  // BUG is HERE
  }
  return user;
}

该函数的作用是 查找已有用户或创建新用户。结账流程会调用它来查找用户。如果账单邮箱与账户邮箱不同,函数会尝试使用账单邮箱创建新用户——这会与已有账户产生冲突。

根本原因: 结账时调用了 findOrCreate,而应该调用 findByEmailfindOrCreate 的行为在注册时是正确的,但在结账时就是错误的。

如果你仅仅对症下药(例如在 create 周围加一个 try‑catch),只会掩盖架构上的错误,后续会产生更多 bug。

第5步:更改(修复)

修复代码以解决根本原因,而不是症状。

// Fix: checkout should find, not create
async checkout(data: CheckoutData) {
  const user = await this.userService.findByEmail(data.billingEmail);
  if (!user) {
    throw new BadRequestError('No account found with this email');
  }
  // ... proceed with checkout
}

第 6 步:评估(验证)

  1. 运行复现步骤 → bug 已消失 ✅
  2. 运行测试套件 → 没有其他问题 ✅
  3. 为此特定 bug 编写测试
test('checkout with non‑matching billing email returns 400', async () => {
  const user = await createUser({ email: 'account@test.com' });
  const res = await api.post('/checkout', {
    ...validCheckoutData,
    billingEmail: 'different@test.com',
    userId: user.id,
  });
  expect(res.status).toBe(400);
  // Verify no new user was created
  const users = await db.users.findAll({ email: 'different@test.com' });
  expect(users).toHaveLength(0);
});

此测试可防止该 bug 再次出现。

高级技巧

技巧:Printf 调试器(这并不丢人)

console.log 调试常被诟病,但只要系统化使用,它是最实用的工具。

不要这样做:

console.log('here');
console.log('here2');
console.log(thing);

应该这样做:

console.log('[CHECKOUT] Input:', {
  email: data.email,
  itemsCount: data.items.length,
});

添加清晰、带上下文的前缀,只记录你需要的数据。这样可以保持输出可读,便于快速发现异常。

调试清单与技巧

1️⃣ 删除调试日志

console.log('[CHECKOUT] User lookup result:', { found: !!user, userId: user?.id });
console.log('[CHECKOUT] Payment attempt:', { amount, currency, method });

操作: 完成后,运行以下命令将代码库中所有这些日志删除:

grep -r "console.log.*\[CHECKOUT\]" -l | xargs sed -i '/console\.log.*\[CHECKOUT\]/d'

2️⃣ 技巧:橡皮鸭解释

目标: 用普通英文解释 bug。

“结账页面在账单邮箱与已有用户匹配时显示‘账户已存在’。结账流程调用 findOrCreate,该函数在不该创建用户时尝试创建用户。这个问题出现在我们把账单邮箱设为结账表单的独立字段之后。”

大多数情况下,仅仅把问题说出来就能触发解决方案。

3️⃣ 技巧:差异调查

# 工作版本与出错版本之间有什么变化?
git diff v2.3.0..v2.3.1 -- src/checkout/

# 谁改动了结账流程?
git log --oneline v2.3.0..v2.3.1 -- src/checkout/

# 那个特定提交做了什么?
git show abc123

提示: bug 是由某一次具体改动引入的。定位到该改动,就找到了根本原因。

4️⃣ 技巧:环境切换

问题: bug 只在生产环境出现?把生产环境带到本地机器上。

# 将生产数据复制到本地(已脱敏!)
pg_dump production_db --exclude-table=secrets | psql local_db

# 在本地回放生产流量
# (工具:GoReplay、mitmproxy)

# 对比配置文件
diff config/production.yml config/local.yml

注意: 与一位了解情况的同事简短交流,往往能省下数小时的猜测时间。

5️⃣ 社区提问

你常用的调试技巧是什么?我发现大多数人在“划分”这一步就直接跳到阅读所有代码。你有系统化的方法吗,还是 console.log 战士?

欢迎留言讨论!

6️⃣ 额外资源

💡 AI 编码提示包 – 超过 50 条实战提示、6 个 Cursor 规则文件以及 Claude Code 工作流模板。
在 Gumroad 上获取 – $9,终身更新。

0 浏览
Back to Blog

相关文章

阅读更多 »