最近同事用 AI 写了一段看起来非常合理的代码,上线后直接把线上的多语言翻译全干没了。

逻辑很简单:每次发版时,先从远端拉取最新的翻译 JSON,和本地的合并,然后一起提交上去。这样就能保证新增的翻译和已有的翻译共存。伪代码大概是这样:

async function updateTranslation(local) {
  const remote = await fetchRemoteJSON();
  const merged = { ...remote, ...local };
  await upload(merged);
}

看起来没毛病对吧?AI 写的也很利索,一把过。

但问题出在 fetchRemoteJSON 失败的时候。AI 很”贴心”地加了兜底逻辑:请求失败就返回空对象,保证流程不中断。

async function fetchRemoteJSON() {
  try {
    const res = await fetch(REMOTE_URL);
    return await res.json();
  } catch (e) {
    console.warn('拉取远端翻译失败,使用空对象兜底');
    return {};
  }
}

于是某次发版时远端接口刚好抖了一下,remote 拿到了 {},合并后只剩本次新增的几条翻译,然后被完整地提交上去——之前积累的上千条翻译,没了。

正确的做法是拉取失败时直接中断流程,而不是吞掉错误继续跑。拉不到最新数据,就不该继续合并和提交。让流程报错、让人介入,远比”优雅降级”后悄悄覆盖数据要安全得多。

AI 在写代码时有一个明显的倾向:让代码跑通。它会本能地给每个可能出错的地方加 try-catch,给每个可能为空的值加默认值。这在很多场景下是合理的,但在某些场景下,这种”兜底”本身就是 bug。像这个案例,空对象不是一个安全的兜底值——它意味着”没有任何历史翻译”,和”拉取失败”是两码事。AI 没有区分这两种语义,只是机械地保证了函数不会抛错。

review AI 代码时,多问一句:这个 catch 里的兜底值,在业务上意味着什么?它真的安全吗? 不是所有的错误都该被吞掉,有时候报错才是正确的行为。