TypeScript 的 try/catch 为什么拿不到错误类型?

在 catch (e) 里,e 的类型是 unknown。你看着 Promise<User> 的签名,完全不知道这个函数会因为什么失败。

原因是:throw 不进类型,Promise<A> 只有成功值的位置,没有失败的位置。 Effect 的做法是多开一个位置,Effect<A, E, R> 里的 E 专门放可预期的失败。

先看一个具体函数,就能看出这个缺口有多大。

一个函数能有哪几种失败?

async function getUser(id: string): Promise<User> {
  const row = await db.query("select * from users where id = ?", [id])
  if (!row) throw new Error("not found")
  const profile = await fetch(`${AVATAR_URL}/${id}`)
  return { ...row, avatar: await profile.text() }
}

至少三种:数据库连接出错、用户不存在、头像服务返回异常。它们全部变成 throw,调用者只能这样接:

try {
  const user = await getUser("42")
} catch (e) {
  // e: unknown —— 是查无此人?网络超时?还是我自己的 bug?
}

这会带来什么问题?

两个。

第一,可预期的失败(用户不存在)和不可预期的缺陷(空指针)被混在同一个通道里,处理代码只能靠 instanceof 或者字符串匹配去猜。

第二,编译器不会提醒你漏掉了某种失败:新增一种失败方式,所有调用点都照样编译通过。

这像一份没有列出过敏原的菜单,菜名写了,能不能吃要吃过才知道。

Effect 把失败放在哪里?

放在第二个类型参数 E:

declare const getUser: (id: string) =>
  Effect.Effect<User, NotFound | NetworkError, Database>

E 是联合类型。读这一行:成功得到 User;可能因为查无此人或网络错误失败,不会有别的可预期失败。

如果某处新增了一种失败,E 会自动变宽,所有没处理它的调用点都会得到类型提示。这个检查不靠自觉,靠类型推导。

那 bug 也会出现在 E 里吗?

不会。Effect 不消除错误,只是让错误变得可见、可分类。缺陷(bug)依然存在,但它们不在 E 里,这正是把「可预期的失败」和「缺陷」分开的意义。具体的区分方法见 错误即类型,另一篇 类型化错误 也讲这个话题。

回头看,这只是 async 签名缺口的其中一处。如果你想看全貌,包括依赖和取消,读 已经有 async/await 了,为什么还要用 Effect?。

本文的 API 名称以 effect 4.0.5 为准。代码是说明用的示意,我没有运行过;缺陷与 E 的区分只是概括,没有自己验证,细节以对应章节为准。