errore について
errore は、例外を投げずにエラーを返したい TypeScript 開発者向けの Go スタイルのエラー処理ライブラリです。`Error | T` のユニオンと `instanceof` による型の絞り込みを使い、タグ付きエラーやマッチング機能も備えます。
主な機能
- 01`Error | T` のユニオンでエラーを返却
- 02`instanceof` による型の絞り込み
- 03タグ付きドメインエラーの作成
- 04`cause` によるエラーチェーンの保持
- 05タグによるエラーの照合
活用シーン
- 01TypeScript 関数から想定される失敗を返す
- 02外部ライブラリの例外をドメインエラー値に変換
- 03早期リターンでフラットな制御フローを記述
- 04ドメインエラーをレスポンスに対応付け
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ remorses/ errore --skill erroreインストール後、Claude Code で/erroreを入力して読み込まれたか確認します。
詳細
errore
TypeScript 向けの Go 風エラーハンドリングです。関数はエラーをスローする代わりに返します。ただし、Go の 2 値タプル(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'を使います。名前空間インポートを使い、分割代入はしません -
想定内の失敗に対してスローしてはいけません。エラーは値として返します
-
unknown | Errorを返してはいけません。ユニオンがunknownに集約され、型の絞り込みが機能しなくなります。よくある落とし穴:res.json()はunknownを返すため、return await res.json()とすると戻り値の型がMyError | unknown→unknownになります。asでキャストして修正します →return (await res.json()) as User -
例外を値に変換するのは境界だけにします。非同期処理には
.catch()、同期処理にはerrore.tryを使います(境界処理 を参照)。制御フローにtry-catchを使ってはいけません -
境界での catch は必ず
causeを含むタグ付きドメインエラーでラップします。.catch((e) => new MyError({ cause: e }))のようにします。.catch((e) => e as Error)を使ってはいけません。また、causeを省略してはいけません(デバッグや中断の検出に必要なエラーチェーンが保持されます) -
ドメインエラーには
createTaggedErrorを使います(エラーの定義 を参照)。エラーをラップするにはcauseを使います -
オプション値には
| undefinedではなく| nullを使います。instanceof Error、=== null、値の順に、3 通りで絞り込みます -
letと try-catch ではなく、constと式を使います(文より式 を参照) -
if分岐の中でエラーを処理して早期リターンし、正常系はルートのインデントに保ちます(フラットな制御フロー を参照) -
matchErrorには必ずErrorハンドラーを含めます。通常のErrorインスタンスに対する必須のフォールバックです -
errore.tryFnよりも常にerrore.tryを優先します。同じ関数ですが、errore.tryが標準の名前です -
中断:カスタム中断エラーは必ず
errore.AbortErrorを継承し、errore.isAbortErrorで検出します(error.name === 'AbortError'は使いません)。また、instanceof Errorより前に、独立した早期リターンとしてisAbortError(result)をチェックします(中断とキャンセル を参照) -
エラーの早期リターンの後で再代入してはいけません。TypeScript は元の変数の型を絞り込むため、
const narrowed = resultというエイリアスは冗長です -
instanceof Errorの早期リターンは 1 行で記述します。if (result instanceof Error) return resultのようにします。分岐内に複数の文がある場合に限り、{ブロックを使います -
伝播させないエラーは必ずログに記録します。エラー分岐で
returnやthrowをしない場合(continueを使うループ、フォールバック分岐、fire-and-forget 呼び出しなど)は、失敗の記録を残すために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 コマンド、または不足しているフラグを含めます。HTTP レスポンスと CLI 出力に適用します。内部エラー(DB の失敗、解析エラーなど)は技術的な内容のままでかまいません。
class UnauthorizedError extends errore.createTaggedError({ name: 'UnauthorizedError', message: 'Not logged in. Sign in at $loginUrl or run: $loginCommand', }) {}
TypeScript のルール
-
位置引数よりオブジェクト引数 — 2 つ以上のパラメーターを持つ関数では、
(id, retries)ではなく({id, retries})を使います -
文より式 —
letとミューテーションの代わりに、IIFE、三項演算子、.map/.filterを使います -
早期リターン — 最初にチェックしてリターンし、ネストさせないでください。条件をまとめます。
if (a) { if (b) }ではなくif (a && b)とします -
anyは使わない — 適切な型を探し、as unknown as Tは最後の手段としてのみ使います -
テンプレート文字列ではなく
cause—new Error(`msg ${e}`)ではなくnew Error("msg", { cause: e })を使います -
初期化していない
letは使わない —let x; if (...) { x = ... }の代わりに、リターンを含む IIFE を使います -
空の配列には型を指定する —
const items = []ではなくconst items: string[] = []とします -
Node の組み込みモジュールにはモジュールインポートを使う —
import fs from 'node:fs'の後にfs.readFileSync(...)を使い、名前付きインポートは使いません -
TypeScript の戻り値の型推論を活用する — 推論された型は常に正しいです。読みやすさが本当に向上する場合(複雑なユニオン型、公開 API の境界など)や、推論された型が意図したものより広い場合にのみ注釈を付けます
-
.filter(Boolean)ではなく.filter(isTruthy)—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 です。2 つの分岐(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)
}
パターン
文より式
後から代入する let より、式を使った const を常に優先します。これにより可変状態がなくなり、制御フローが明確になります。複雑さに応じて使い分けます。
シンプル:三項演算子
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 は必要ありません。
非同期処理の境界では、プロミスに直接 .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 型をネストせずに、3 つの異なる状態を表せます。
並列処理
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)— try/finally を using に置き換える
try/finally には構造上の問題があります。リソースが増えるたびにネストの階層が増えることです。リソースが 2 つあればインデントも 2 階層になり、クリーンアップ処理はリソースを取得した場所から離れた finally ブロックに分散します。await using と DisposableStack を使えば関数をフラットに保てます。リソースごとに cleanup.defer() を 1 つ記述するだけで、リソースが 1 つでも 10 個でもインデントは同じです。正常な return、早期のエラー return、例外など、どの終了経路でもクリーンアップが自動的に逆順(LIFO)で実行されます。
tsconfig の要件: lib に "ESNext.Disposable" を追加します。
{
"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()
}
リソースを追加する際は、ネストの階層を増やすのではなく、1 行(
cleanup.defer())を追加します。errore のポリフィルがランタイムを処理し、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 } を返します。インラインで分割代入し、まず truthy チェックで error を確認します(多くの SDK は通常のオブジェクトを返すため、instanceof は使いません)。次にタグ付きエラーでラップし、その後 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チェーンを利用できます。
Partition:成功と失敗の分割
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() で受け取られます。したがって、errore.AbortError を継承する型付きエラーを必ず渡してください。通常の Error や文字列を渡すと、isAbortError で検出できません。
中止エラーの検出には、必ず errore.isAbortError(error) を使用してください。タグ付きの中止エラーではタグが .name になるため、error.name === 'AbortError' を手動で確認しないでください。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は、次の 3 種類の中止を検出します。(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)
}
signal.abortedのチェックは、負荷の高い操作(ネットワーク、データベースへの書き込み、ファイル I/O)の前に配置してください。シグナルを受け取った非同期呼び出しの後にisAbortErrorをチェックしてください。どちらも、関数がキャンセルにすばやく応答できるようにします。
リンティング
プロジェクトで lintcn を使用している場合は、破棄された Error | T の戻り値を検出する no-unhandled-error ルールについて、docs/lintcn.md を参照してください。
注意点
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