关于 errore
errore 是一个面向 TypeScript 的 Go 风格错误处理库,适合希望将错误作为返回值而非异常抛出的开发者。它使用 Error 与值的联合类型和 instanceof 检查,并提供带标签的错误及匹配工具。
主要功能
- 01以 Error | T 联合类型返回错误
- 02通过 instanceof 缩窄类型
- 03创建带标签的领域错误
- 04用 cause 保留错误链
- 05按错误标签进行匹配
适用场景
- 01在 TypeScript 函数中显式返回预期错误
- 02将第三方异常转换为领域错误值
- 03以早退方式编写扁平控制流
- 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
规则
每条规则只有一行;详细说明仅在下方的模式章节中出现一次。
-
始终使用
import * as errore from 'errore'——命名空间导入,绝不解构导入 -
对预期内的失败绝不抛出错误——将错误作为值返回
-
绝不返回
unknown | Error——联合类型会折叠为unknown,导致类型收窄失效。常见陷阱:res.json()返回unknown,因此return await res.json()会使返回类型变为MyError | unknown→unknown。修复方法:使用as进行类型断言 →return (await res.json()) as User -
仅在边界处将异常转换为值——异步操作使用
.catch(),同步操作使用errore.try(参见边界:异步用 catch,同步用 errore.try)。绝不使用try-catch进行控制流处理 -
始终将边界捕获的错误包装为带标签的领域错误,并设置
cause——.catch((e) => new MyError({ cause: e }))。绝不使用.catch((e) => e as Error),也绝不省略cause(它能保留错误链,便于调试和检测中止) -
使用
createTaggedError定义领域错误(参见定义错误);使用cause包装错误 -
可选值使用
| null,而非| undefined——这样可以分三步收窄:instanceof Error、=== null,然后是值本身 -
使用
const和表达式,绝不使用let和 try-catch(参见表达式优先于语句) -
在
if分支内处理错误,并提前退出,让正常路径保持在最外层缩进(参见扁平控制流) -
在
matchError中始终包含Error处理器——这是处理普通Error实例所必需的回退分支 -
始终优先使用
errore.try,而不是errore.tryFn——两者功能相同,errore.try是规范名称 -
中止:自定义中止错误必须继承
errore.AbortError,并使用errore.isAbortError检测(绝不使用error.name === 'AbortError');在instanceof Error之前,必须单独提前返回isAbortError(result)(参见中止与取消) -
错误提前返回后不要重新赋值——TypeScript 会收窄原变量;使用
const narrowed = result创建别名是多余的 -
instanceof Error的提前返回始终写在一行中——if (result instanceof Error) return result。仅当分支包含多条语句时才使用{块 -
对未传播的错误始终记录日志——如果错误分支不会
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) } -
面向用户的错误消息始终要提示解决方案——提供登录 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