戻り値の型として `Promise
コードレビューをしていて、次のようなコードに遭遇したことはないだろうか。
// 良くあるアンチパターン
async function fetchUserData(userId: string): Promise
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(‘Failed to fetch user’); // ここで例外をスローしている
}
return response.json();
}
一見、何の問題もないように見えるかもしれない。しかし、この関数を呼び出す側(Consumer)の視点に立った瞬間、TypeScriptの型システムにおける「隠された罠」が牙をむく。
シグネチャが `Promise
今回は、非同期関数の戻り値とエラーを型レベルで完全に制御し、呼び出し元に安全なエラーハンドリングを強制するプロダクションクオリティの設計パターンを解説する。
—
1. なぜ `throw` は型システムを破壊するのか?
TypeScriptのコンパイラは、関数の戻り値の型(例: `Promise
そのため、`async/await` や `Promise` を使うコードでは、以下の2つの大きな問題が生じる。
1. 例外の握りつぶし: 呼び出し元が `try/catch` を書き忘れても、コンパイルエラーにならない。
2. `any` の蔓延: キャッチしたエラー(`catch (error)`)の型はデフォルトで `unknown`(あるいは古い設定では `any`)になり、`error.message` にアクセスするために煩雑な型ガードを毎回書かされる。
これらを解決するためのアプローチが、「例外を値(Value)として扱う(Either / Result型パターン)」だ。
—
2. 設計の核心:Result型による非同期エラーの型安全化
関数が失敗する可能性を、例外(Exception)ではなく「戻り値のバリエーション」として表現する。これにより、コンパイラは呼び出し元に対して「成功と失敗の両方のケースをハンドリングしなさい」と強制できるようになる。
以下のプロダクションコードを見てほしい。これが、私たちが現場で採用すべきモダンな非同期エラーハンドリングの基準だ。
プロダクションコード例
/
- 処理の成功を表す型
/
export type Success
readonly success: true;
readonly data: T;
};
/
- 処理の失敗(エラー)を表す型
- E はエラーのドメイン型を制限するために設ける
/
export type Failure
readonly success: false;
readonly error: E;
};
/
- 成功と失敗を内包するResult型
/
export type Result
/
- 非同期処理を安全にラップし、例外をResult型に変換するユーティリティ関数
- @param promise 実行するPromise
- @returns Result
型のPromise
/
export async function safeAsync
promise: Promise
): Promise
try {
const data = await promise;
return { success: true, data };
} catch (error) {
// 予期せぬ非同期エラー(stringやunknownなど)をE型に安全にキャスト・調停する
return { success: false, error: error as E };
}
}
—
3. 型評価とコンパイル時の振る舞い
上記の `Result
// ドメイン固有のエラー定義
type UserNotFoundError = { readonly _tag: ‘UserNotFoundError’; readonly userId: string };
type NetworkError = { readonly _tag: ‘NetworkError’; readonly message: string };
type FetchUserError = UserNotFoundError | NetworkError;
// ユーザー取得関数(内部で例外を投げる可能性があるとする)
async function fetchUser(userId: string): Promise
if (userId === ”) {
throw { _tag: ‘UserNotFoundError’, userId } as UserNotFoundError;
}
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) {
throw { _tag: ‘NetworkError’, message: ‘Network failed’ } as NetworkError;
}
return res.json();
}
// ==========================================
// 呼び出し元のコード(Consumer)
// ==========================================
async function handleLogin(userId: string) {
// safeAsyncを使うことで、戻り値は Promise
const result = await safeAsync
// ここでTypeScriptの「区別された共用体(Discriminated Unions)」の型ガードが発動する
if (!result.success) {
// コンパイラはここで result が Failure
switch (result.error._tag) {
case ‘UserNotFoundError’:
console.warn(`User not found: ${result.error.userId}`);
return;
case ‘NetworkError’:
console.error(`Network issue: ${result.error.message}`);
return;
}
}
// ここに到達した時点で、result.data は確実に User 型として安全に扱える
console.log(`Welcome back, ${result.data.name}`);
}
この設計がもたらす圧倒的なメリット
1. `try/catch` の呪縛からの解放: 非同期関数の中で何がスローされようとも、呼び出し元は `try` ブロックを書く必要がない。すべてが型安全な `if (!result.success)` の分岐に収束する。
2. 網羅性の担保(Exhaustiveness Checking): `switch` 文や `if` 分岐において、エラーパターンのハンドリング漏れが発生した場合、TypeScriptコンパイラが警告(あるいは `never` 型を使った網羅性チェック)を出すことが可能になる。
—
4. パフォーマンス上の注意点とV8エンジン最適化の視点
「すべての非同期処理をオブジェクト(`Success` / `Failure`)でラップする」ことに対して、シニアエンジニアならこう疑問に思うはずだ。
「毎回の非同期呼び出しでオブジェクトの生成コスト(GCのプレッシャー)が発生しないか?」
結論から言えば、一般的なWebフロントエンドや通常のNode.js APIサーバーの負荷において、この程度のオブジェクトアロケーションによるパフォーマンス低下は完全に無視できるレベルである。V8エンジンのジェネレーション別GC(Garbage Collector)は、短命なオブジェクト(Young Generation)の回収を極めて高速に行う。
ただし、極限のスループットが要求される基盤ミドルウェアや、1秒間に数十万回実行されるホットパス(Hot Path)においては話が別だ。
その場合は、無駄なオブジェクト生成を避けるために、以下のような「キャッシュ済み成功オブジェクト」や「型の最適化」を検討する余地はある。
// 高頻度実行パス用のイミュータブルな定数(必要に応じた最適化の例)
const EMPTY_SUCCESS = Object.freeze({ success: true, data: undefined } as const);
だが、ビジネスロジックの複雑性とコードの保守性を天秤にかけたとき、「実行時エラーのバグゼロ化」というメリットは、微小なCPUコストを何倍も上回るリターンをもたらす。
—
5. テクニカルリードからの総括
非同期エラーハンドリングにおける型定義の甘さは、フロントエンドのサイレントバグ(画面のフリーズ、無限ローディング、未定義プロパティアクセスによるクラッシュ)の最大の温床である。
「たまたま例外が発生しなかった」という曖昧な状態を排除し、「成功と失敗のどちらのルートも型として明示的に実装しなければコンパイルが通らない」という堅牢な構造をチームに導入すること。それこそが、TypeScriptを真に使いこなし、プロダクトの品質を次元進化させるチーフアーキテクトの仕事である。
今日のコードレビューから、あなたのプロジェクトの `async/await` に `Result` 型の思想を取り入れてみてほしい。コードの美しさと静的解析の安心感に、開発フィールが劇的に変わるはずだ。