mcpField
E
errore

errore

github.com/remorses开发285184 次安装3 周前更新
代码仓库

关于 errore

errore 是一个面向 TypeScript 的 Go 风格错误处理库,适合希望将错误作为返回值而非异常抛出的开发者。它使用 Error 与值的联合类型和 instanceof 检查,并提供带标签的错误及匹配工具。

主要功能

  1. 01以 Error | T 联合类型返回错误
  2. 02通过 instanceof 缩窄类型
  3. 03创建带标签的领域错误
  4. 04用 cause 保留错误链
  5. 05按错误标签进行匹配

适用场景

  1. 01在 TypeScript 函数中显式返回预期错误
  2. 02将第三方异常转换为领域错误值
  3. 03以早退方式编写扁平控制流
  4. 04将领域错误映射为响应

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/remorses/errore --skill errore

安装后在 Claude Code 中输入/errore确认已加载。

详细介绍

errore

为 TypeScript 提供 Go 风格的错误处理。函数返回错误,而不是抛出错误——但你返回的不是 Go 的双值元组(val, err),而是单个 Error | T 联合类型。你无需检查 err != nil,而是检查 instanceof Error。TypeScript 会自动收窄类型。不需要包装类型或 Result 单子,只需联合类型和 instanceof。

const user = await getUser(id)
if (user instanceof Error) return user // early return, like Go
console.log(user.name) // TypeScript knows: User

规则

每条规则只有一行;详细说明仅在下方的模式章节中出现一次。

  1. 始终使用 import * as errore from 'errore'——命名空间导入,绝不解构导入

  2. 对预期内的失败绝不抛出错误——将错误作为值返回

  3. 绝不返回 unknown | Error——联合类型会折叠为 unknown,导致类型收窄失效。常见陷阱:res.json() 返回 unknown,因此 return await res.json() 会使返回类型变为 MyError | unknown → unknown。修复方法:使用 as 进行类型断言 → return (await res.json()) as User

  4. 仅在边界处将异常转换为值——异步操作使用 .catch(),同步操作使用 errore.try(参见边界:异步用 catch,同步用 errore.try)。绝不使用 try-catch 进行控制流处理

  5. 始终将边界捕获的错误包装为带标签的领域错误,并设置 cause——.catch((e) => new MyError({ cause: e }))。绝不使用 .catch((e) => e as Error),也绝不省略 cause(它能保留错误链,便于调试和检测中止)

  6. 使用 createTaggedError 定义领域错误(参见定义错误);使用 cause 包装错误

  7. 可选值使用 | null,而非 | undefined——这样可以分三步收窄:instanceof Error、=== null,然后是值本身

  8. 使用 const 和表达式,绝不使用 let 和 try-catch(参见表达式优先于语句)

  9. 在 if 分支内处理错误,并提前退出,让正常路径保持在最外层缩进(参见扁平控制流)

  10. 在 matchError 中始终包含 Error 处理器——这是处理普通 Error 实例所必需的回退分支

  11. 始终优先使用 errore.try,而不是 errore.tryFn——两者功能相同,errore.try 是规范名称

  12. 中止:自定义中止错误必须继承 errore.AbortError,并使用 errore.isAbortError 检测(绝不使用 error.name === 'AbortError');在 instanceof Error 之前,必须单独提前返回 isAbortError(result)(参见中止与取消)

  13. 错误提前返回后不要重新赋值——TypeScript 会收窄原变量;使用 const narrowed = result 创建别名是多余的

  14. instanceof Error 的提前返回始终写在一行中——if (result instanceof Error) return result。仅当分支包含多条语句时才使用 { 块

  15. 对未传播的错误始终记录日志——如果错误分支不会 return 或 throw(例如使用 continue 的循环、回退分支、即发即弃调用),就添加 console.warn/console.error,确保失败留有记录。传播出去的错误无需记录日志——由调用方处理:

    const emailResult = await sendEmail(user.email).catch(
      (e) => new EmailError({ email: user.email, cause: e }),
    )
    if (emailResult instanceof Error) {
      console.warn('Failed to send email:', emailResult.message)
    }
    
  16. 面向用户的错误消息始终要提示解决方案——提供登录 URL、CLI 命令或缺少的标志,让 Agent 和用户能够自行解决,而不必猜测。适用于 HTTP 响应和 CLI 输出;内部错误(数据库故障、解析错误)可以保持技术性:

    class UnauthorizedError extends errore.createTaggedError({
      name: 'UnauthorizedError',
      message: '未登录。请访问 $loginUrl 登录,或运行:$loginCommand',
    }) {}
    

TypeScript 规则

  • 优先使用对象参数,而非位置参数——对于有 2 个或更多参数的函数,使用 ({id, retries}),而非 (id, retries)

  • 优先使用表达式,而非语句——使用 IIFE、三元表达式、.map/.filter,而非 let + 修改变量

  • 尽早返回——在函数开头进行检查并返回,不要嵌套。合并条件:使用 if (a && b),而非 if (a) { if (b) }

  • 不要使用 any——查找合适的类型,只有在万不得已时才使用 as unknown as T

  • 使用 cause,而非模板字符串——使用 new Error("msg", { cause: e }),而非 new Error(`msg ${e}`)

  • 不要使用未初始化的 let——使用带返回值的 IIFE,而非 let x; if (...) { x = ... }

  • 为数组指定类型——使用 const items: string[] = [],而非 const items = []

  • 使用模块方式导入 Node 内置模块——使用 import fs from 'node:fs',然后调用 fs.readFileSync(...),而非命名导入

  • 让 TypeScript 推断返回类型——推断出的类型总是正确的。仅当注解确实能提升可读性(复杂联合类型、公共 API 边界),或推断出的类型比预期更宽泛时,才添加注解

  • 使用 .filter(isTruthy),而非 .filter(Boolean)——Boolean 不会缩窄类型,因此过滤后 (T | null)[] 仍为 (T | null)[]。请使用类型守卫:

    function isTruthy<T>(value: T): value is NonNullable<T> {
      return Boolean(value)
    }
    const items = results.filter(isTruthy)
    

扁平控制流

尽量减少代码块嵌套。每一层缩进都会增加认知负担。理想的函数应从上到下在根级别展开——进行检查并尽早返回,不使用 else、嵌套 if 或 try-catch。

核心模式——调用 → 检查错误 → 有错误则退出 → 在根级别继续。这是最重要的结构规则。它与 Go 的 if err != nil { return err } 代码块结构相同——读者沿左侧扫描时会直接略过这些障碍:

const user = await getUser(id)
if (user instanceof Error) return user

const posts = await getPosts(user.id)
if (posts instanceof Error) return posts

return render(user, posts)

不使用 else——尽早返回即可消除它:if (x) return 'A'; return 'B'

不使用 else if 链——改用一系列尽早返回的 if 代码块:

function getStatus(code: number): string {
  if (code === 200) return 'ok'
  if (code === 404) return 'not found'
  if (code >= 500) return 'server error'
  return 'unknown'
}

扁平化嵌套的 if——反转条件并尽早返回。if (A) { if (B) { ... } } 可改为 if (!A) return; if (!B) return; ...。取最外层的 if 条件,将其取反并返回失败情况,然后在根级别继续。对每个嵌套的 if 重复此操作。成功路径自然执行至函数末尾。

避免将 try-catch 用于控制流——try-catch 是造成嵌套的最大问题。它强制形成双分支结构(try + catch),并隐藏具体是哪一行抛出了异常。请在边界处将异常转换为值:

async function loadConfig(): Promise<Config> {
  const raw = await fs
    .readFile('config.json', 'utf-8')
    .catch((e) => new ConfigError({ reason: 'Read failed', cause: e }))
  if (raw instanceof Error) return { port: 3000 }

  const parsed = errore.try(
    () => JSON.parse(raw) as Config,
    (e) => new ConfigError({ reason: 'Invalid JSON', cause: e }),
  )
  if (parsed instanceof Error) return { port: 3000 }

  if (!parsed.port) return { port: 3000 }

  return parsed
}

在分支中处理错误,让成功路径保持在根级别——始终在 if 代码块中处理错误,绝不在其中编写成功逻辑。错误处理应放在带有尽早退出的分支中。将成功逻辑放进 if 代码块会颠倒流程,并掩盖成功路径。如果看到条件中有 !(x instanceof Error),说明模式用反了——请将其反转。

循环中也一样——在 if 中处理错误并使用 continue,让成功路径保持扁平:

for (const id of ids) {
  const item = await fetchItem(id)
  if (item instanceof Error) {
    console.warn('Skipping', id, item.message)
    continue
  }
  await processItem(item)
  results.push(item)
}

模式

优先使用表达式,而非语句

始终优先使用带表达式的 const,而非先声明 let、稍后再赋值。这样可以消除可变状态,并明确控制流。根据复杂度逐步选择:

简单:三元表达式

const user = fetchResult instanceof Error ? fallbackUser : fetchResult

中等:带尽早返回的 IIFE——当三元表达式嵌套过深或涉及多项检查时,使用 IIFE。它会将所有中间变量限制在作用域内,并通过尽早返回提高清晰度:

const config: Config = (() => {
  const envResult = loadFromEnv()
  if (!(envResult instanceof Error)) return envResult
  const fileResult = loadFromFile()
  if (!(fileResult instanceof Error)) return fileResult
  return defaultConfig
})()

所有 let x; if (...) { x = ... } 都可以改写为 const x = ternary 或 const x: T = (() => { ... })()。IIFE 模式在 errore 代码中很常见——它通过尽早返回让错误处理保持扁平,同时生成单个不可变绑定。

定义错误
import * as errore from 'errore'

class NotFoundError extends errore.createTaggedError({
  name: 'NotFoundError',
  message: 'User $id not found in $database',
}) {}

createTaggedError 会自动提供 _tag、带类型的 $variable 属性、cause、findCause、toJSON、指纹,以及静态 .is() 类型守卫。 省略 message 可让调用方在构造时提供消息:new MyError({ message: 'details' })。指纹会保持稳定。 模板中不能使用的保留变量名:$_tag、$name、$stack、$cause。

实例属性:

err._tag // 'NotFoundError'
err.id // 'abc' (from $id)
err.database // 'users' (from $database)
err.message // 'User abc not found in users'
err.messageTemplate // 'User $id not found in $database'
err.fingerprint // ['NotFoundError', 'User $id not found in $database']
err.cause // original error if wrapped
err.toJSON() // structured JSON with all properties
err.findCause(DbError) // walks .cause chain, returns typed match or undefined
NotFoundError.is(val) // static type guard
返回错误
async function getUser(id: string) {
  const user = await db.findUser(id)
  if (!user) return new NotFoundError({ id, database: 'users' })
  return user
}

返回错误,不要抛出错误。返回类型会明确告知调用方可能出错的情况。

边界(异步使用 .catch,同步使用 errore.try)

.catch() 和 errore.try 只应出现在调用栈的最低层,也就是与不受你控制的代码(第三方库、JSON.parse、fetch、文件 I/O 等)交界的位置。你自己的函数会将错误作为值返回,因此不需要 .catch() 或 try。

对于异步边界:直接在 Promise 上使用 .catch((e) => new MyError({ cause: e }))。对于同步边界:使用 errore.try(() => ..., (e) => ...)。.catch() 回调接收的是 any(Promise 拒绝没有类型),但将其包装在类型化错误中,就能为联合类型提供具体类型,无需使用 as 断言。TypeScript 会自动推断联合类型:

async function getUser(id: string) {
  const res = await fetch(`/users/${id}`).catch(
    (e) => new NetworkError({ url: `/users/${id}`, cause: e }),
  )
  if (res instanceof Error) return res

  if (!res.ok) return new NetworkError({ url: `/users/${id}`, reason: `HTTP ${res.status}` })

  const data = await (res.json() as Promise<UserPayload>).catch(
    (e) => new NetworkError({ url: `/users/${id}`, cause: e }),
  )
  if (data instanceof Error) return data

  if (!data.active) return new InactiveUserError({ id })
  return { ...data, displayName: `${data.first} ${data.last}` }
}

将 .catch() 和 errore.try 视为抛出异常的世界(外部代码)与 errore 世界(将错误作为值)之间的适配器。在边界处将异常转换为值后,上层代码就只需进行普通的 instanceof 检查。

可选值(| null)
async function findUser(email: string): Promise<DbError | User | null> {
  const result = await db
    .query(email)
    .catch((e) => new DbError({ message: 'Query failed', cause: e }))
  if (result instanceof Error) return result
  return result ?? null
}

// Caller: three-way narrowing
const user = await findUser('[email protected]')
if (user instanceof Error) return user
if (user === null) return
console.log(user.name) // User

Error | T | null 提供三种不同状态,无需嵌套 Result 和 Option 类型。

并行操作
const [userResult, postsResult, statsResult] = await Promise.all([
  getUser(id),
  getPosts(id),
  getStats(id),
])

if (userResult instanceof Error) return userResult
if (postsResult instanceof Error) return postsResult
if (statsResult instanceof Error) return statsResult

return { user: userResult, posts: postsResult, stats: statsResult }

单独检查每个结果。你能准确知道哪个操作失败了。

穷尽匹配(matchError)
const response = errore.matchError(error, {
  NotFoundError: (e) => ({
    status: 404,
    body: { error: `${e.table} ${e.id} not found` },
  }),
  DbError: (e) => ({ status: 500, body: { error: 'Database error' } }),
  Error: (e) => ({ status: 500, body: { error: 'Unexpected error' } }),
})
return res.status(response.status).json(response.body)

matchError 根据 _tag 路由,并要求提供一个 Error 回退处理程序,以处理普通的 Error 实例。只需处理部分情况时,请使用 matchErrorPartial。

资源清理(defer)——使用 using 替代 try/finally

try/finally 有一个结构性问题:每个资源都会增加一层嵌套。两个资源就会产生两层缩进,而且清理代码分散在 finally 块中,离资源获取的位置很远。await using + DisposableStack 能让函数保持扁平——每个资源使用一行 cleanup.defer(),无论有一个还是十个资源,缩进都相同。无论以何种方式退出,清理都会自动按相反顺序(LIFO)执行,包括正常返回、提前返回错误或抛出异常。

**tsconfig 要求:**将 "ESNext.Disposable" 添加到 lib:

{
  "compilerOptions": {
    "lib": ["ES2022", "ESNext.Disposable"],
  },
}
async function importData(url: string, dbUrl: string): Promise<ImportError | { rows: number }> {
  await using cleanup = new errore.AsyncDisposableStack()

  const db = await connectDb(dbUrl).catch((e) => new ImportError({ reason: 'db connect', cause: e }))
  if (db instanceof Error) return db
  cleanup.defer(() => db.close())

  const tmpFile = await createTempFile()
  cleanup.defer(() => tmpFile.delete())

  const response = await fetch(url).catch((e) => new ImportError({ reason: 'fetch', cause: e }))
  if (response instanceof Error) return response

  await tmpFile.write(await response.text())
  await db.import(tmpFile.path)
  return { rows: await db.count() }
  // cleanup: tmpFile.delete() → db.close()
}

添加资源只需一行(cleanup.defer()),而不是再增加一层嵌套。errore polyfill 会处理运行时,tsconfig 中的 lib 项则负责处理类型。

回退值
const result = errore.try(() =>
  JSON.parse(fs.readFileSync('config.json', 'utf-8')),
)
const config = result instanceof Error ? { port: 3000, debug: false } : result

对 instanceof Error 使用三元表达式即可替代 let + try-catch。单个表达式,无需修改变量,也没有中间状态。

遍历原因链(findCause)
const dbErr = error.findCause(DbError)
if (dbErr) {
  console.log(dbErr.host) // type-safe access
}

// Or standalone function for any Error
const dbErr = errore.findCause(error, DbError)

findCause 会先检查错误本身,再递归遍历 .cause。它会返回匹配的错误,并完整推断其类型;若无匹配项,则返回 undefined。同时可安全处理循环引用。

自定义基类
class AppError extends Error {
  statusCode = 500
  toResponse() {
    return { error: this.message, code: this.statusCode }
  }
}

class NotFoundError extends errore.createTaggedError({
  name: 'NotFoundError',
  message: 'Resource $id not found',
  extends: AppError,
}) {
  statusCode = 404
}

const err = new NotFoundError({ id: '123' })
err.toResponse() // { error: 'Resource 123 not found', code: 404 }
err instanceof AppError // true
err instanceof Error // true

使用 extends 在所有领域错误中继承共用功能(HTTP 状态码、日志方法、响应格式化)。

与旧代码的边界
async function legacyHandler(id: string) {
  const user = await getUser(id)
  if (user instanceof Error) throw new Error('Failed to get user', { cause: user })
  return user
}

在旧代码需要异常的边界处,检查 instanceof Error,并使用 cause 抛出错误。这样既能保留错误链,也能保持模式一致。

转换 { data, error } 返回值

一些 SDK(Supabase、Stripe 等)返回 { data, error },而不是抛出错误。直接解构,先用真值检查(不要使用 instanceof——大多数 SDK 返回的是普通对象)检查 error,再将其包装为带标签的错误,然后继续处理 data:

const { data, error } = await supabase.from('users').select('*').eq('id', id)
if (error) return new SupabaseError({ cause: error })
if (data === null) return new NotFoundError({ id })
// data is narrowed here

将错误包装为领域错误,比直接返回 SDK 的错误更好——这样可以获得 _tag、类型化属性和 cause 链。

分区:拆分成功与失败结果
const allResults = await Promise.all(ids.map((id) => fetchItem(id)))
const [items, errors] = errore.partition(allResults)

errors.forEach((e) => console.warn('Failed:', e.message))
// items contains only successful results, fully typed

partition 会将 (Error | T)[] 数组拆分为 [T[], Error[]]。无需手动累积。

中止与取消

controller.abort(reason) 会原样抛出 reason——你传入什么,.catch() 收到的就是什么。这意味着你 MUST 传入一个扩展 errore.AbortError 的类型化错误,绝不能传入普通的 Error 或字符串,否则 isAbortError 无法识别它。

始终使用 errore.isAbortError(error) 检测中止错误——切勿手动检查 error.name === 'AbortError',因为带标签的中止错误会将标签作为 .name。isAbortError 会遍历整个 .cause 链,因此即使中止错误被 .catch() 包装,也能正常工作。

保持中止检查扁平:先单独将 isAbortError(result) 作为提前返回条件,再单独将 result instanceof Error 作为另一个提前返回条件。绝不要将其中一个嵌套在另一个里面。

import * as errore from 'errore'

class TimeoutError extends errore.createTaggedError({
  name: 'TimeoutError',
  message: 'Request timed out for $operation',
  extends: errore.AbortError,
}) {}

const controller = new AbortController()
const timer = setTimeout(
  () => controller.abort(new TimeoutError({ operation: 'fetch' })),
  5000,
)

const res = await fetch(url, { signal: controller.signal }).catch(
  (e) => new NetworkError({ url, cause: e }),
)
clearTimeout(timer)

if (errore.isAbortError(res)) return res
if (res instanceof Error) return res

isAbortError 会检测三种中止情况:(1)直接调用 controller.abort() 产生的原生 DOMException;(2)直接的 errore.AbortError 实例;(3)扩展 errore.AbortError 的带标签错误——即使它们被包装在另一个错误的 .cause 链中。

中止时提前返回(检查 signal.aborted)

在执行副作用或异步操作之前检查 signal.aborted——这与错误处理使用相同的提前返回模式,但适用于取消。缺少这些检查时,已取消的工作仍会继续运行。

for (const item of items) {
  if (signal.aborted) return                    // before work
  const data = await fetchData(item.id, { signal })
    .catch((e) => new FetchError({ id: item.id, cause: e }))
  if (errore.isAbortError(data)) return         // after async
  if (data instanceof Error) { console.warn(data.message); continue }
  if (signal.aborted) return                    // before write
  await db.save(data)
}

在耗时操作(网络、数据库写入、文件 I/O)之前检查 signal.aborted。在接收了该信号的异步调用之后检查 isAbortError。两者都能确保函数及时响应取消。

代码检查

如果项目使用 lintcn,请阅读 docs/lintcn.md,了解可捕获被丢弃的 Error | T 返回值的 no-unhandled-error 规则。

易错点

当 CustomError 扩展 Error 时,CustomError | Error 存在歧义
// BAD: both sides of the union are Error instances
type Result = MyCustomError | Error
// instanceof Error matches BOTH — can't distinguish success from failure
// Success types must never extend Error

评论

支持 Markdown