fail-fast について
fail-fastは、エラーを既定値や無言の処理で隠さず、発生箇所で明確にするコーディングスキルです。エラー処理を書く、またはレビューする開発者向けです。
主な機能
- 01入力の早期検証
- 02不変条件のアサート
- 03予期しないエラーの伝播
- 04境界でのエラー処理
活用シーン
- 01エラー処理コードのレビュー
- 02無言のフォールバックの置き換え
- 03不正な状態の拡散防止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yanko-belov/ code-craft --skill fail-fastインストール後、Claude Code で/fail-fastを入力して読み込まれたか確認します。
詳細
速やかに失敗させる
概要
問題が発生したら、すぐに、明確に失敗させます。
デフォルト値を返す try/catch でエラーを隠さないでください。不正な状態を伝播させないでください。失敗した時点で失敗させましょう。破損したデータを抱えたまま、3 層後で失敗させてはいけません。
使用する場面
- エラー処理コードを書くとき
- エラーをキャッチしてデフォルト値を返そうとしているとき
- あらゆる場所に「防御的な」null チェックを追加するとき
- すべてを try/catch で囲もうとしているとき
- スローする代わりにエラーオブジェクトを返そうとしているとき
絶対のルール
NEVER hide failures. Fail loud, fail early.
例外は認められません:
- 「アプリはクラッシュしてはいけない」
- 「スローするより何かを返すほうがよい」
- 「エラーを適切に処理する」
- 「防御的プログラミング」
検出:「握りつぶす」コードの兆候
エラーがひそかに消えてしまうなら、失敗を遅らせています:
// ❌ VIOLATION: Hiding failures
async function processPayment(userId: string, amount: number): Promise<PaymentResult> {
try {
const user = await getUser(userId);
if (!user) return { success: false, error: 'User not found' };
const card = await validateCard(user.cardToken);
if (!card.valid) return { success: false, error: 'Invalid card' };
const result = await chargeCard(card, amount);
if (!result.success) return { success: false, error: 'Payment failed' };
return { success: true, transactionId: result.id };
} catch (error) {
return { success: false, error: 'Internal error' }; // ← SWALLOWED!
}
}
問題点:
- 呼び出し元は何が失敗したのかを把握できません
- スタックトレースが失われます
- バグが「内部エラー」として隠れてしまいます
- 実際の失敗を把握できません
正しいパターン:速やかに失敗させる
失敗した時点でスローします。エラーは伝播させましょう:
// ✅ CORRECT: Fail fast
async function processPayment(userId: string, amount: number): Promise<Transaction> {
// Validate early - fail fast on bad input
if (!userId) throw new ValidationError('userId is required');
if (amount <= 0) throw new ValidationError('amount must be positive');
// Let failures propagate - don't swallow
const user = await getUser(userId);
if (!user) throw new NotFoundError(`User ${userId} not found`);
const card = await validateCard(user.cardToken);
if (!card.valid) throw new PaymentError('Card validation failed', card.errors);
// This might throw - that's okay! Let it.
const transaction = await chargeCard(card, amount);
return transaction;
}
// Caller handles errors appropriately
try {
const tx = await processPayment(userId, amount);
res.json({ success: true, transactionId: tx.id });
} catch (error) {
if (error instanceof ValidationError) {
res.status(400).json({ error: error.message });
} else if (error instanceof NotFoundError) {
res.status(404).json({ error: error.message });
} else if (error instanceof PaymentError) {
res.status(402).json({ error: error.message });
} else {
// Unknown error - log it, return 500
logger.error('Payment failed', error);
res.status(500).json({ error: 'Internal server error' });
}
}
失敗を遅らせることが危険な理由
| 問題 | 影響 |
|---|---|
| 隠れたバグ | エラーが「動かなかった」で済まされます |
| コンテキストの喪失 | スタックトレースには原因ではなく catch が表示されます |
| 状態の破損 | 不正なデータが伝播します |
| デバッグの悪夢 | 実際にはどこで失敗したのでしょうか? |
| 気付かれないデータ損失 | 操作が失敗してもアプリは続行します |
速やかに失敗させるテクニック
1. 早い段階で検証する
function createUser(data: unknown): User {
// Fail IMMEDIATELY on bad input
if (!data || typeof data !== 'object') {
throw new ValidationError('Invalid user data');
}
const { email, name } = data as Record<string, unknown>;
if (!email || typeof email !== 'string') {
throw new ValidationError('Email is required');
}
if (!name || typeof name !== 'string') {
throw new ValidationError('Name is required');
}
// Only proceed with valid data
return new User(email, name);
}
2. 不変条件をアサートする
function withdraw(account: Account, amount: number): void {
// Assert what must be true
assert(amount > 0, 'Withdrawal amount must be positive');
assert(account.balance >= amount, 'Insufficient funds');
account.balance -= amount;
// Post-condition check
assert(account.balance >= 0, 'Balance went negative - invariant violated');
}
3. 型システムを使う
// ❌ Fail slow: null checks everywhere
function processOrder(order: Order | null): void {
if (!order) return; // Silent failure
// ...
}
// ✅ Fail fast: require valid input
function processOrder(order: Order): void {
// If order is null, TypeScript catches it
// If it gets here with null, it will throw - good!
}
プレッシャーに対抗するプロトコル
1. 「アプリはクラッシュしてはいけない」
プレッシャー:「スローするとユーザーにエラーが表示されます」
回答: 明確なエラーをユーザーに表示するほうが、データを破損させたり、失敗を隠したりするよりも適切です。
対応: エラーをスローし、境界(API 層)でキャッチして、適切な HTTP コードを返します。
2. 「スローするより何かを返すほうがよい」
プレッシャー:「エラーオブジェクトを返すほうが関数型です」
回答: 呼び出し元が確認するのであれば、エラーオブジェクトを使っても問題ありません。ただし、たいてい確認されません。
対応: 予期しない失敗にはスローを使います。呼び出し元が両方のケースを実際に処理する場合に限り、Result 型を使います。
3. 「エラーを適切に処理する」
プレッシャー:「適切な処理とはスローしないことです」
回答: 適切な処理とは、状況に合った対応をすることです。握りつぶすのは適切な処理ではありません。
対応: スローし、境界でキャッチして、意味のあるエラーレスポンスを返します。
4. 「防御的プログラミング」
プレッシャー:「防御的なコードはあらゆるケースに対応します」
回答: 防御的であるとは、早い段階で検証して失敗させることです。失敗を隠すことではありません。
対応: 入力を検証し、不変条件をアサートして、違反があればスローします。
危険信号 — 立ち止まって再検討する
次のいずれかに気付いたら、リファクタリングしてください:
catch (e) { return null; }catch (e) { return { success: false }; }if (!x) return;(何も通知せずに早期リターンする)try { } catch { }(空の catch)- エラー時にデフォルト値を返す
- 「Error: Internal error」(汎用的な catch-all)
- エラーをログに記録するだけで、処理を続行する
これらはすべて、エラーを伝播させるか、明示的にスローすべきことを意味します。
クイックリファレンス
| 失敗を遅らせる(悪い例) | 速やかに失敗させる(良い例) |
|---|---|
catch (e) { return null } | catch (e) { throw e } |
if (!user) return | if (!user) throw new NotFoundError() |
return { success: false } | throw new OperationError() |
| 汎用的な「内部エラー」 | 具体的なエラー型 |
| 握りつぶして続行する | 伝播させて境界で処理する |
よくある正当化(すべて誤り)
| 言い訳 | 実際のところ |
|---|---|
| 「アプリはクラッシュしてはいけない」 | 明確なエラーのほうが、隠れたバグよりも適切です。 |
| 「スローする代わりに値を返す」 | 呼び出し元は戻り値を無視します。スローされたエラーは無視できません。 |
| 「エラーを適切に処理する」 | 握りつぶすのは適切な処理ではありません。 |
| 「防御的プログラミング」 | 防御的であるとは、検証して失敗させることであり、隠すことではありません。 |
| 「関数を絶対にクラッシュさせない」 | エラー時にクラッシュさせることでバグを発見できます。 |
| 「ユーザー体験」 | ユーザーは、失敗をひそかに隠されるよりも「決済に失敗しました」と知らされることを望みます。 |
結論
速やかに失敗し、明確に失敗し、発生源で失敗してください。
エラーが発生したら、コンテキストを添えて直ちに例外をスローしてください。エラーは、ログに記録してユーザーに適した応答に変換できる境界まで伝播させてください。決して握りつぶさないでください。失敗を隠すためにデフォルト値を返してはいけません。