npx skills add https://github.com/remorses/errore --skill 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 // 提前返回,类似 Go
console.log(user.name) // TypeScript 知道:User
始终 import * as errore from 'errore' —— 命名空间导入,切勿解构
切勿为预期失败抛出错误 —— 将错误作为值返回
切勿返回 —— 联合类型会坍缩为 ,破坏类型缩小。常见陷阱: 返回 ,因此 会使返回类型变为 → 。修复方法:使用 进行类型转换 →
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
unknown | Errorunknownres.json()unknownreturn await res.json()MyError | unknownunknownasreturn (await res.json()) as User避免使用 try-catch 进行流程控制 —— 对于异步边界使用 .catch(),对于同步边界使用 errore.try
使用 createTaggedError 处理领域错误 —— 提供 _tag、类型化属性、$variable 插值、cause、findCause、toJSON 和指纹生成
让 TypeScript 推断返回类型 —— 仅当显式标注能提高可读性(复杂联合类型、公共 API)或推断产生的类型比预期更宽泛时才添加
使用 cause 包装错误 —— new MyError({ ..., cause: originalError })
对于可选值使用 | null,而非 | undefined —— 三重类型缩小:instanceof Error、=== null,然后是值
使用 const + 表达式,切勿使用 let + try-catch —— 三元运算符、IIFE、instanceof Error
始终在 if 分支内处理错误并提前退出,保持成功路径在根层级 —— 类似 Go 的 if err != nil { return err },检查错误,退出(return/continue/break),然后在顶层缩进级别继续成功路径。这使得成功路径从上到下可读,嵌套最小化
始终在 matchError 中包含 Error 处理器 —— 作为普通 Error 实例的必需后备方案
对于异步边界使用 .catch(),对于同步边界使用 errore.try —— 仅在与不受控依赖项(第三方库、JSON.parse、fetch、文件 I/O)交互的最低调用栈层级使用。你自己的代码应将错误作为值返回,而非抛出。
始终将 .catch() 包装在带标签的领域错误中 —— .catch((e) => new MyError({ cause: e }))。.catch() 回调接收 any,但包装在类型化错误中能为联合类型提供具体类型。切勿使用 .catch((e) => e as Error) —— 始终进行包装。
始终在 .catch() 回调中传递 cause —— .catch((e) => new MyError({ cause: e })),切勿 .catch(() => new MyError())。没有 cause,原始错误会丢失,isAbortError 无法遍历链来检测中止。cause 保留了完整的错误链,便于调试和中止检测。
始终优先使用 errore.try 而非 errore.tryFn —— 它们是同一个函数,但 errore.try 是规范名称
使用 errore.isAbortError 检测中止错误 —— 切勿手动检查 error.name === 'AbortError',因为带标签的中止错误将其标签作为 .name
自定义中止错误必须扩展 errore.AbortError —— 这样即使被 .catch() 包装,isAbortError 也能在原因链中检测到它们
保持中止检查扁平化 —— 首先将 isAbortError(result) 作为独立的提前返回检查,然后将 result instanceof Error 作为单独的提前返回检查。切勿将 isAbortError 嵌套在 instanceof Error 内部:
const result = await fetchData({ signal }).catch(
(e) => new FetchError({ cause: e }),
)
if (errore.isAbortError(result)) return 'Request timed out'
if (result instanceof Error) return Failed: ${result.message}
不要在错误提前返回后重新赋值 —— TypeScript 在 instanceof Error 检查返回后会自动缩小原始变量的类型范围。使用 const narrowed = result 别名是多余的:
const result = await fetch(url).catch((e) => new FetchError({ cause: e }))
if (result instanceof Error) return Failed: ${result.message}
await result.json() // TS 知道此处 result 是 Response
始终记录未传播的错误 —— 当错误分支不 return 或 throw 错误时(即错误被有意忽略),添加 console.warn 或 console.error,以便在调试时能看到失败。静默忽略错误会使 bug 不可见:
// 错误:错误被静默忽略 —— 如果同步失败,你永远不会知道 const result = await syncToCloud(data) if (result instanceof Error) { // 此处为空 —— 静默失败 }
// 正确:在继续之前记录 —— 错误在日志中可见
const result = await syncToCloud(data)
if (result instanceof Error) {
console.warn('Cloud sync failed:', result.message)
}
已传播的错误(
return error)无需记录 —— 调用者会处理它们。但你选择忽略的错误必须留下痕迹。这适用于带有continue的循环、后备分支以及任何有意丢弃错误的路径。
对象参数优于位置参数 —— 对于具有 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 推断返回类型 —— 默认情况下不要标注返回类型。TypeScript 会从代码中推断,推断出的类型总是正确的。仅当确实能提高可读性(复杂联合类型、公共 API 边界)或推断产生的类型比预期更宽泛时才添加显式返回类型:
// 让推断完成其工作
function getUser(id: string) { const user = await db.find(id) if (!user) return new NotFoundError({ id }) return user }
// 当显式标注能为复杂的公共 API 增加清晰度时 function processRequest( req: Request, ): Promise<ValidationError | AuthError | DbError | null | Response> { // ... }
使用.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)
controller.abort()必须使用类型化错误 —— abort(reason) 按原样抛出 reason。必须传递一个扩展 errore.AbortError 的带标签错误,切勿使用 new Error() 或字符串 —— 否则 isAbortError 无法在原因链中检测到它:
class TimeoutError extends errore.createTaggedError({
name: 'TimeoutError', message: 'Request timed out for $operation', extends: errore.AbortError, }) {} controller.abort(new TimeoutError({ operation: 'fetch' }))
切勿静默抑制错误 —— 空的 catch {} 和未记录的 error 分支会隐藏失败。使用 errore 时你很少需要 catch,但在任何错误未被传播的边界,始终记录它(参见规则 20):
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) }
保持代码块嵌套最小化。每一级缩进都是认知负担。理想的函数在根层级从上到下阅读 —— 检查和提前返回,没有 else,没有嵌套的 if,没有 try-catch。
核心模式 —— 调用 → 检查错误 → 如果错误则退出 → 在根层级继续。这是最重要的单一结构规则。
Go:
user, err := getUser(id)
if err != nil {
return fmt.Errorf("get user: %w", err)
}
// user 在此处有效,在根层级
posts, err := getPosts(user.ID)
if err != nil {
return fmt.Errorf("get posts: %w", err)
}
// posts 在此处有效,在根层级
return render(user, posts)
errore(相同结构):
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)
读者扫描函数的左边缘以跟随成功路径 —— 就像阅读 Go 函数一样,其中 if err != nil 块是你跳过的减速带。
不使用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({
try: () => JSON.parse(raw) as Config,
catch: (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),说明你反转了模式 —— 翻转它。
保持成功路径在最小缩进级别 —— 读者扫描左边缘以跟随主要逻辑:
async function handleRequest(req: Request): Promise<AppError | Response> {
const body = await parseBody(req)
if (body instanceof Error) return body
const user = await authenticate(req.headers)
if (user instanceof Error) return user
const permission = checkPermission(user, body.resource)
if (permission instanceof Error) return permission
const result = await execute(body.action, body.resource)
if (result instanceof Error) return result
return new Response(JSON.stringify(result), { status: 200 })
}
循环中同理 —— 错误在 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' (来自 $id)
err.database // 'users' (来自 $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 // 如果包装了原始错误
err.toJSON() // 包含所有属性的结构化 JSON
err.findCause(DbError) // 遍历 .cause 链,返回类型化匹配或 undefined
NotFoundError.is(val) // 静态类型守卫
async function getUser(id: string) {
const user = await db.findUser(id)
if (!user) return new NotFoundError({ id, database: 'users' })
return user
}
返回错误,不要抛出它。返回类型告诉调用者可能出错的具体情况。
const user = await getUser(id)
if (user instanceof Error) return user
const posts = await getPosts(user.id)
if (posts instanceof Error) return posts
return posts
每个错误都在发生时检查。TypeScript 在每次检查后缩小类型范围。
async function fetchJson<T>(url: string): Promise<NetworkError | T> {
const response = await fetch(url).catch(
(e) => new NetworkError({ url, reason: 'Fetch failed', cause: e }),
)
if (response instanceof Error) return response
if (!response.ok) {
return new NetworkError({ url, reason: `HTTP ${response.status}` })
}
const data = await (response.json() as Promise<T>).catch(
(e) => new NetworkError({ url, reason: 'Invalid JSON', cause: e }),
)
return data
}
Promise 上的
.catch()将拒绝转换为类型化错误。TypeScript 自动推断联合类型(Response | NetworkError)。对于同步边界(JSON.parse等)使用errore.try。
.catch() 和 errore.try 应仅出现在调用栈的最低层级 —— 正好在你无法控制的代码(第三方库、JSON.parse、fetch、文件 I/O 等)的边界处。你自己的函数永远不应抛出,因此它们永远不需要 .catch() 或 try。
对于异步边界:直接在 Promise 上使用 .catch((e) => new MyError({ cause: e }))。TypeScript 自动推断联合类型。对于同步边界:使用 errore.try({ try: () => ..., catch: (e) => ... })。.catch() 回调接收 any(Promise 拒绝是无类型的),但包装在类型化错误中能为联合类型提供具体类型 —— 无需 as 断言。
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
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检查。你自己的函数将错误作为值返回 —— 它们永远不需要.catch()或try。
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
}
// 调用者:三重类型缩小
const user = await findUser('alice@example.com')
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 }
每个结果单独检查。你确切知道哪个操作失败。
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。
using 替换 try/finallytry/finally 存在一个结构性问题:每个资源都会增加一个嵌套层级。两个资源 = 两个缩进层级。业务逻辑随着每个资源被埋得更深,并且清理工作分散在远离资源获取位置的 finally 块中。await using + DisposableStack 保持函数扁平化 —— 每个资源一个 cleanup.defer(),无论你有一个资源还是十个资源,缩进级别都相同。清理会在每个退出路径上自动按相反顺序运行。
tsconfig 要求: 将 "ESNext.Disposable" 添加到 lib:
{
"compilerOptions": {
"lib": ["ES2022", "ESNext.Disposable"],
},
}
之前 —— 嵌套的 try/finally:
async function importData(url: string, dbUrl: string) {
const db = await connectDb(dbUrl)
try {
const tmpFile = await createTempFile()
try {
const data = await (await fetch(url)).text()
await tmpFile.write(data)
await db.import(tmpFile.path)
return { rows: await db.count() }
} finally {
await tmpFile.delete()
}
} finally {
await db.close()
}
}
之后 —— 使用await using扁平化:
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()
}
await using保证在每个退出路径上执行清理 —— 正常返回、提前错误返回或异常。资源按 LIFO 顺序释放。添加一个资源只需一行(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。单个表达式,无突变,无中间状态。
const dbErr = error.findCause(DbError)
if (dbErr) {
console.log(dbErr.host) // 类型安全访问
}
// 或用于任何 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 } 而非抛出异常。内联解构,首先检查 error(真值检查,非 instanceof —— 大多数 SDK 返回普通对象),包装在带标签的错误中,然后继续处理 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 在此处已缩小类型
如果 SDK 的
error已经是Error实例,你可以直接返回它,但包装在领域错误中更好 —— 提供_tag、类型化属性和cause链。使用真值检查检查error,而非instanceof Error,因为大多数 SDK 错误对象是普通对象。
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 仅包含成功结果,完全类型化
partition将(Error | T)[]数组拆分为[T[], Error[]]。无需手动累加。
controller.abort(reason) 按原样抛出 reason —— 你传递什么,.catch() 就接收什么。这意味着你必须传递一个扩展 errore.AbortError 的类型化错误,切勿使用普通 Error 或字符串。
始终使用 errore.isAbortError(error) 检测中止错误。它会遍历整个 .cause 链,因此即使中止错误被 .catch() 包装也能工作。
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 —— 与错误处理相同的提前返回模式,但用于取消。没有这些,已取消的工作会继续运行。
for (const item of items) {
if (signal.aborted) return // 工作前
const data = await fetchData(item.id, { signal })
.catch((e) => new FetchError({ id: item.id, cause: e }))
if (errore.isAbortError(data)) return // 异步后
if (data instanceof Error) { console.warn(data.message); continue }
if (signal.aborted) return // 写入前
await db.save(data)
}
在昂贵操作(网络、数据库写入、文件 I/O)之前放置
signal.aborted检查。在接收到信号的异步调用之后检查isAbortError。两者都使函数对取消保持响应。
如果项目使用 lintcn,请阅读 docs/lintcn.md 了解 no-unhandled-error 规则,该规则会捕获被丢弃的 Error | T 返回值。
// 错误:联合类型的双方都是 Error 实例
type Result = MyCustomError | Error
// instanceof Error 匹配两者 —— 无法区分成功与失败
// 成功类型绝不能扩展 Error
每周安装量
77
代码仓库
GitHub 星标
239
首次出现
2026年2月20日
安全审计
安装于
codex76
opencode75
gemini-cli73
github-copilot73
amp73
kimi-cli73
Go-style error handling for TypeScript. Functions return errors instead of throwing them — but instead of Go's two-value tuple (val, err), you return a single Error | T union. Instead of checking err != nil, you check instanceof Error. TypeScript narrows the type automatically. No wrapper types, no Result monads, just unions and instanceof.
const user = await getUser(id)
if (user instanceof Error) return user // early return, like Go
console.log(user.name) // TypeScript knows: User
Always import * as errore from 'errore' — namespace import, never destructure
Never throw for expected failures — return errors as values
Never return unknown | Error — the union collapses to unknown, breaks narrowing. Common trap: res.json() returns unknown, so return await res.json() makes the return type MyError | unknown → unknown. Fix: cast with as → return (await res.json()) as User
Avoid try-catch for control flow — use .catch() for async boundaries, errore.try for sync boundaries
Use createTaggedError for domain errors — gives you _tag, typed properties, $variable interpolation, cause, findCause, toJSON, and fingerprinting
Let TypeScript infer return types — only add explicit annotations when they improve readability (complex unions, public APIs) or when inference produces a wider type than intended
Use cause to wrap errors — new MyError({ ..., cause: originalError })
Use | null for optional values, not | undefined — three-way narrowing: instanceof Error, === null, then value
Use const + expressions, never let + try-catch — ternaries, IIFEs, instanceof Error
Always handle errors inside if branches with early exits, keep the happy path at root — like Go's if err != nil { return err }, check the error, exit (return/continue/break), and continue the success path at the top indentation level. This makes the happy path readable top-to-bottom with minimal nesting
Always include Error handler in matchError — required fallback for plain Error instances
Use .catch() for async boundaries, errore.try for sync boundaries — only at the lowest call stack level where you interact with uncontrolled dependencies (third-party libs, JSON.parse, fetch, file I/O). Your own code should return errors as values, not throw.
Always wrap .catch() in a tagged domain error — .catch((e) => new MyError({ cause: e })). The .catch() callback receives any, but wrapping in a typed error gives the union a concrete type. Never use .catch((e) => e as Error) — always wrap.
Always pass cause in .catch() callbacks — .catch((e) => new MyError({ cause: e })), never .catch(() => new MyError()). Without cause, the original error is lost and isAbortError can't walk the chain to detect aborts. The cause preserves the full error chain for debugging and abort detection.
Always prefer errore.try over errore.tryFn — they are the same function, but errore.try is the canonical name
Use errore.isAbortError to detect abort errors — never check error.name === 'AbortError' manually, because tagged abort errors have their tag as .name
Custom abort errors MUST extend errore.AbortError — so isAbortError detects them in the cause chain even when wrapped by .catch()
Keep abort checks flat — check isAbortError(result) first as its own early return, then result instanceof Error as a separate early return. Never nest isAbortError inside instanceof Error:
const result = await fetchData({ signal }).catch(
(e) => new FetchError({ cause: e }),
)
if (errore.isAbortError(result)) return 'Request timed out'
if (result instanceof Error) return Failed: ${result.message}
Don't reassign after error early returns — TypeScript narrows the original variable automatically after instanceof Error checks return. A const narrowed = result alias is redundant:
const result = await fetch(url).catch((e) => new FetchError({ cause: e }))
if (result instanceof Error) return Failed: ${result.message}
await result.json() // TS knows result is Response here
Always log errors that are not propagated — when an error branch doesn't return or throw the error (i.e. the error is intentionally swallowed), add a console.warn or console.error so failures are visible during debugging. Silent error swallowing makes bugs invisible:
// BAD: error silently ignored — if sync fails you'll never know const result = await syncToCloud(data) if (result instanceof Error) { // nothing here — silent failure }
// GOOD: log before continuing — error is visible in logs
const result = await syncToCloud(data)
if (result instanceof Error) {
console.warn('Cloud sync failed:', result.message)
}
Propagated errors (
return error) don't need logging — the caller handles them. But errors you choose to ignore must leave a trace. This applies to loops withcontinue, fallback branches, and any path where the error is intentionally dropped.
Object args over positional — ({id, retries}) not (id, retries) for functions with 2+ params
Expressions over statements — use IIFEs, ternaries, .map/.filter instead of let + mutation
Early returns — check and return at top, don't nest. Combine conditions: if (a && b) not if (a) { if (b) }
Noany — search for proper types, use as unknown as T only as last resort
Keep block nesting minimal. Every level of indentation is cognitive load. The ideal function reads top to bottom at root level — checks and early returns, no else, no nested if, no try-catch.
Core pattern — call → check error → exit if error → continue at root. This is the single most important structural rule.
Go:
user, err := getUser(id)
if err != nil {
return fmt.Errorf("get user: %w", err)
}
// user is valid here, at root level
posts, err := getPosts(user.ID)
if err != nil {
return fmt.Errorf("get posts: %w", err)
}
// posts is valid here, at root level
return render(user, posts)
errore (identical structure):
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)
The reader scans the left edge of the function to follow the happy path — just like reading a Go function where if err != nil blocks are speed bumps you skip over.
Noelse — early return eliminates it: if (x) return 'A'; return 'B'
Noelse if chains — sequence of early-return if blocks:
function getStatus(code: number): string {
if (code === 200) return 'ok'
if (code === 404) return 'not found'
if (code >= 500) return 'server error'
return 'unknown'
}
Flatten nestedif — invert conditions and return early. if (A) { if (B) { ... } } becomes if (!A) return; if (!B) return; .... The transformation rule: take the outermost if condition, negate it, return the failure case, then continue at root level. Repeat for each nested if. The happy path falls through to the end.
Avoidtry-catch for control flow — try-catch is the worst offender for nesting. It forces a two-branch structure (try + catch) and hides which line threw. Convert exceptions to values at boundaries:
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({
try: () => JSON.parse(raw) as Config,
catch: (e) => new ConfigError({ reason: 'Invalid JSON', cause: e }),
})
if (parsed instanceof Error) return { port: 3000 }
if (!parsed.port) return { port: 3000 }
return parsed
}
Errors in branches, happy path at root — always handle errors inside if blocks, never success logic. Error handling goes in branches with early exits. Putting success logic inside if blocks inverts the flow and buries the happy path. If you see!(x instanceof Error) in a condition, you've inverted the pattern — flip it.
Keep the happy path at minimum indentation — the reader scans down the left edge to follow the main logic:
async function handleRequest(req: Request): Promise<AppError | Response> {
const body = await parseBody(req)
if (body instanceof Error) return body
const user = await authenticate(req.headers)
if (user instanceof Error) return user
const permission = checkPermission(user, body.resource)
if (permission instanceof Error) return permission
const result = await execute(body.action, body.resource)
if (result instanceof Error) return result
return new Response(JSON.stringify(result), { status: 200 })
}
Same in loops — error in if + continue, happy path flat:
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)
}
Always prefer const with an expression over let assigned later. This eliminates mutable state and makes control flow explicit. Escalate by complexity:
Simple: ternary
const user = fetchResult instanceof Error ? fallbackUser : fetchResult
Medium: IIFE with early returns — when a ternary gets too nested or involves multiple checks, use an IIFE. It scopes all intermediate variables and uses early returns for clarity:
const config: Config = (() => {
const envResult = loadFromEnv()
if (!(envResult instanceof Error)) return envResult
const fileResult = loadFromFile()
if (!(fileResult instanceof Error)) return fileResult
return defaultConfig
})()
Every
let x; if (...) { x = ... }can be rewritten asconst x = ternaryorconst x: T = (() => { ... })(). The IIFE pattern is idiomatic in errore code — it keeps error handling flat with early returns while producing a single immutable binding.
import * as errore from 'errore'
class NotFoundError extends errore.createTaggedError({
name: 'NotFoundError',
message: 'User $id not found in $database',
}) {}
createTaggedErrorgives you_tag, typed$variableproperties,cause,findCause,toJSON, fingerprinting, and a static.is()type guard — all for free. Omitmessageto let the caller provide it at construction time:new MyError({ message: 'details' }). The fingerprint stays stable. Reserved variable names that cannot be used in templates:$_tag,$name, , .
Instance properties:
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
}
Return the error, don't throw it. The return type tells callers exactly what can go wrong.
const user = await getUser(id)
if (user instanceof Error) return user
const posts = await getPosts(user.id)
if (posts instanceof Error) return posts
return posts
Each error is checked at the point it occurs. TypeScript narrows the type after each check.
async function fetchJson<T>(url: string): Promise<NetworkError | T> {
const response = await fetch(url).catch(
(e) => new NetworkError({ url, reason: 'Fetch failed', cause: e }),
)
if (response instanceof Error) return response
if (!response.ok) {
return new NetworkError({ url, reason: `HTTP ${response.status}` })
}
const data = await (response.json() as Promise<T>).catch(
(e) => new NetworkError({ url, reason: 'Invalid JSON', cause: e }),
)
return data
}
.catch()on a promise converts rejections to typed errors. TypeScript infers the union (Response | NetworkError) automatically. Useerrore.tryfor sync boundaries (JSON.parse, etc.).
.catch() and errore.try should only appear at the lowest level of your call stack — right at the boundary with code you don't control (third-party libraries, JSON.parse, fetch, file I/O, etc.). Your own functions should never throw, so they never need .catch() or try.
For async boundaries: use .catch((e) => new MyError({ cause: e })) directly on the promise. TypeScript infers the union automatically. For sync boundaries: use errore.try({ try: () => ..., catch: (e) => ... }). The .catch() callback receives any (Promise rejections are untyped), but wrapping in a typed error gives the union a concrete type — no as assertions needed.
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
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}` }
}
Think of
.catch()anderrore.tryas the adapter between the throwing world (external code) and the errore world (errors as values). Once you've converted exceptions to values at the boundary, everything above is plaininstanceofchecks. Your own functions return errors as values — they never need.catch()ortry.
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('alice@example.com')
if (user instanceof Error) return user
if (user === null) return
console.log(user.name) // User
Error | T | nullgives you three distinct states without nesting Result and Option types.
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 }
Each result is checked individually. You know exactly which operation failed.
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)
matchErrorroutes by_tagand requires anErrorfallback for plain Error instances. UsematchErrorPartialwhen you only need to handle some cases.
usingtry/finally has a structural problem: every resource adds a nesting level. Two resources = two levels of indentation. The business logic gets buried deeper with each resource, and cleanup is split across finally blocks far from where the resource was acquired. await using + DisposableStack keeps the function flat — one cleanup.defer() per resource, same indentation whether you have one resource or ten. Cleanup runs automatically in reverse order on every exit path.
tsconfig requirement: add "ESNext.Disposable" to lib:
{
"compilerOptions": {
"lib": ["ES2022", "ESNext.Disposable"],
},
}
Before — nested try/finally:
async function importData(url: string, dbUrl: string) {
const db = await connectDb(dbUrl)
try {
const tmpFile = await createTempFile()
try {
const data = await (await fetch(url)).text()
await tmpFile.write(data)
await db.import(tmpFile.path)
return { rows: await db.count() }
} finally {
await tmpFile.delete()
}
} finally {
await db.close()
}
}
After — flat withawait using:
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()
}
await usingguarantees cleanup on every exit path — normal return, early error return, or exception. Resources release in LIFO order. Adding a resource is one line (cleanup.defer()), not another nesting level. The errore polyfill handles the runtime; the tsconfiglibentry handles the types.
const result = errore.try(() =>
JSON.parse(fs.readFileSync('config.json', 'utf-8')),
)
const config = result instanceof Error ? { port: 3000, debug: false } : result
Ternary on
instanceof Errorreplaceslet+ try-catch. Single expression, no mutation, no intermediate state.
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)
findCausechecks the error itself first, then walks.causerecursively. Returns the matched error with full type inference, orundefined. Safe against circular references.
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
Use
extendsto inherit shared functionality (HTTP status codes, logging methods, response formatting) across all your domain errors.
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
}
At boundaries where legacy code expects exceptions, check
instanceof Errorand throw withcause. This preserves the error chain and keeps the pattern consistent.
{ data, error } ReturnsSome SDKs (Supabase, Stripe, etc.) return { data, error } instead of throwing. Destructure inline, check error first (truthy, not instanceof — most SDKs return plain objects), wrap in a tagged error, then continue with 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
If the SDK's
erroris already anErrorinstance you can return it directly, but wrapping in a domain error is better — gives you_tag, typed properties, andcausechain. Checkerrorwith truthy check, notinstanceof Error, since most SDK error objects are plain objects.
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
partitionsplits an array of(Error | T)[]into[T[], Error[]]. No manual accumulation.
controller.abort(reason) throws reason as-is — whatever you pass is what .catch() receives. This means you MUST pass a typed error extending errore.AbortError, never a plain Error or string.
Always use errore.isAbortError(error) to detect abort errors. It walks the entire .cause chain, so it works even when the abort error is wrapped by .catch().
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
isAbortErrordetects three kinds of abort: (1) nativeDOMExceptionfrom barecontroller.abort(), (2) directerrore.AbortErrorinstances, (3) tagged errors that extenderrore.AbortError— even when wrapped in another error's.causechain.
Check signal.aborted before side effects or async operations — same early-return pattern as errors but for cancellation. Without these, cancelled work keeps running.
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)
}
Place
signal.abortedchecks before expensive operations (network, db writes, file I/O). CheckisAbortErrorafter async calls that received the signal. Both keep the function responsive to cancellation.
If the project uses lintcn, read docs/lintcn.md for the no-unhandled-error rule that catches discarded Error | T return values.
// 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
Weekly Installs
77
Repository
GitHub Stars
239
First Seen
Feb 20, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex76
opencode75
gemini-cli73
github-copilot73
amp73
kimi-cli73
Android 整洁架构指南:模块化设计、依赖注入与数据层实现
1,400 周安装
cause not template strings — new Error("msg", { cause: e }) not new Error(msg ${e})
No uninitializedlet — use IIFE with returns instead of let x; if (...) { x = ... }
Type empty arrays — const items: string[] = [] not const items = []
Module imports for node builtins — import fs from 'node:fs' then fs.readFileSync(...), not named imports
Let TypeScript infer return types — don't annotate return types by default. TypeScript infers them from the code and the inferred type is always correct. Only add an explicit return type when it genuinely improves readability (complex unions, public API boundaries) or when inference produces a wider type than intended:
// let inference do its job
function getUser(id: string) { const user = await db.find(id) if (!user) return new NotFoundError({ id }) return user }
// explicit annotation when it adds clarity on a complex public API function processRequest( req: Request, ): Promise<ValidationError | AuthError | DbError | null | Response> { // ... }
.filter(isTruthy) not .filter(Boolean) — Boolean doesn't narrow types, so (T | null)[] stays (T | null)[] after filtering. Use a type guard:
function isTruthy<T>(value: T): value is NonNullable<T> {
return Boolean(value) } const items = results.filter(isTruthy)
controller.abort() must use typed errors — abort(reason) throws reason as-is. MUST pass a tagged error extending errore.AbortError, NEVER new Error() or a string — otherwise isAbortError can't detect it in the cause chain:
class TimeoutError extends errore.createTaggedError({
name: 'TimeoutError', message: 'Request timed out for $operation', extends: errore.AbortError, }) {} controller.abort(new TimeoutError({ operation: 'fetch' }))
Never silently suppress errors — empty catch {} and unlogged error branches hide failures. With errore you rarely need catch at all, but at any boundary where an error is not propagated, always log it (see rule 20):
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) }
$stack$cause