二十个联系人本应在 CRM 中被归档,但 UI 却闪烁着绿色的成功标志,而操作却悄无声息地失败了。一个让 Supabase-JS mutation 中的 error 字段变为必填项的 TypeScript 辅助函数,现在迫使开发者直面失败,而不是将其掩盖。
为什么 Supabase-JS 的返回结构是一个陷阱
Supabase 的 JavaScript 客户端 (@supabase/supabase-js) 在数据库写入被拒绝时不会抛出异常。相反,它会通过一个对象 { data, error } 来 resolve promise。如果行级安全性 (RLS) 策略、唯一键冲突或任何其他约束阻止了查询,data 将返回 null,而 error 则包含数据库消息。客户端假设调用者会检查 error;它永远不会中止调用。
在实践中,许多代码库将此类调用视为“发后即忘”(fire-and-forget)的操作:
await supabase
.from('contacts')
.update({ statut: 'ancien', archived_at: now })
.eq('id', id)
return { ok: true }
当 update 被 RLS 规则拦截时,promise 仍然会 resolve。由于开发者从未解构 { error },失败变得不可见。函数返回 { ok: true },UI 显示成功,而数据保持不变。没有日志出现,没有 Sentry 警报触发,这个 bug 可能会潜伏数日之久。
仅靠 Linter 是不够的
静态分析工具可以在忽略 error 属性时发出警告,但它们无法强制执行运行时契约。开发者仍然可以编写 const _ = await … 来消除警告,或者添加注释来抑制规则。根本问题在于,类型系统允许在完全不提及 error 的情况下让调用成功。
mutate() 辅助函数:让错误处理成为强制要求
作者构建了一个名为 mutate() 的小型包装器,它改变了返回类型的结构。该辅助函数不再返回 { data, error },而是返回一个元组 [data, error],其中 error 是一个必填字段。这样一来,TypeScript 就会拒绝编译任何丢弃第二个元素的调用。
async function mutate<T>(promise: Promise<{ data: T | null; error: any }>) {
const { data, error } = await promise
// Send error to Sentry immediately
if (error) Sentry.captureException(error)
return [data, error] as const
}
用法变得显式:
const [result, err] = await mutate(
supabase
.from('contacts')
.update({ statut: 'ancien', archived_at: now })
.eq('id', id)
)
if (err) {
// Handle or rethrow
return { ok: false, message: err.message }
}
return { ok: true, data: result }
如果开发者忘记捕获 err,TypeScript 会报错:“长度为 2 的元组类型 [T, any] 在索引 1 处没有元素。” 在解决错误之前,代码将无法通过编译。该辅助函数还注入了 Sentry 监测(instrumentation),确保即使调用者随后忽略了错误,每一次数据库拒绝操作都会被记录下来。
涉及的关键问题
- 数据完整性 – 无声的失败会让无效状态悄悄进入生产环境。一个从未从活跃列表中移除的已归档联系人可能会导致下游报告出错。
- 用户信任 – 当 UI 声称成功但实际并未发生任何变化时,会削弱用户的信心。客户看到“已归档”,但在搜索中仍然能找到该联系人。
- 开发者时间 – 追踪虚幻的 bug 会消耗大量时间。显式的错误处理能在故障发生点暴露问题,从而缩短调试周期。
- 运维成本 – 与无声数据丢失事件的代价相比,增加几行代码和一个小型包装器几乎可以忽略不计。
权衡
该辅助函数增加了代码的冗余度:每个 mutation 调用现在都会返回一个元组,且调用者必须编写 if (err) 代码块。一些团队可能会认为这是模板代码(boilerplate)噪音,特别是对于那些预期会成功的简单 CRUD 操作。反驳的观点是,这些额外的代码是一种保障,而非可选功能。在数据正确性至关重要的环境(如 CRM 系统、金融、医疗)中,强制进行检查是非常值得的。
下一步计划
作者承诺在第三篇文章中探讨 RLS 策略在没有任何解释的情况下返回零行数据的情况。这种模式与“无声错误”案例类似,会将失败隐藏在看似成功的查询之后。该系列文章旨在揭示 Supabase API 的“传闻”一面,并为开发者提供要求获取事实真相的具体工具。
核心总结: Supabase-JS 的设计使得除非你记得读取 error 字段,否则失败的写入看起来就像成功一样。通过使用 TypeScript 强制执行的辅助函数来包装调用,使 error 成为必填项并将其记录到 Sentry 中,你可以将无声的失败转化为可见的、可操作的事件。代码量的适度增加换来的是数据的可靠性和用户的信任——这两者是任何“无声的成功”都无法提供的。
