mcpField
E
error-boundaries

error-boundaries

github.com/yanko-belov開発1335 回インストール40 週間前に更新
リポジトリ

error-boundaries について

error-boundaries は、ビジネスロジック全体に捕捉処理を散在させず、意図した境界でエラーを処理する方法を示すソフトウェア設計スキルです。アプリケーションの障害処理やコンポーネントの縮退設計を行う開発者向けです。

主な機能

  1. 01アーキテクチャ上のエラー境界を特定
  2. 02捕捉箇所とエラー伝播を案内
  3. 03境界でのグレースフルデグラデーションを解説
  4. 04チェックリストと境界の例を提示

活用シーン

  1. 01HTTP やコマンドライン入口のエラー処理を設計
  2. 02外部サービスアダプターの障害を分離
  3. 03任意機能にフォールバック動作を用意

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/yanko-belov/code-craft --skill error-boundaries

インストール後、Claude Code で/error-boundariesを入力して読み込まれたか確認します。

詳細

エラーバウンダリー

概要

コールスタック内のランダムな箇所ではなく、論理的な境界でエラーを捕捉します。

エラーバウンダリーは、連鎖的な障害を防ぎながら、適切な縮退運転を可能にする戦略的な捕捉ポイントです。ビジネスロジック全体に散在させるのではなく、アーキテクチャ上の境界に配置します。

使用する場面

  • アプリケーションアーキテクチャを設計するとき
  • try/catch をどこに置くか決めるとき
  • 1つの障害ですべてがクラッシュするのを防ぐとき
  • 適切な縮退運転を実装するとき
  • コンポーネント同士を分離するとき

厳守すべきルール

NEVER scatter try/catch randomly. Place catches at ARCHITECTURAL BOUNDARIES only.

例外はありません:

  • 「防御的プログラミング」のためではありません
  • 「安全のためのラッパー」のためではありません
  • 「念のため」ではありません
  • 「関数が例外をスローするかもしれない」からではありません

境界は意図的に設けるものです。ランダムな捕捉はバグを隠します。

境界とは?

境界とは、コンテキストが変わるポイントです:

┌─────────────────────────────────────────────────────────────┐
│                         ENTRY BOUNDARIES                     │
│  HTTP Request → [BOUNDARY] → Application                    │
│  Message Queue → [BOUNDARY] → Handler                       │
│  CLI Command → [BOUNDARY] → Execution                       │
│  Cron Job → [BOUNDARY] → Task                               │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                       INTERNAL BOUNDARIES                    │
│  Application → [BOUNDARY] → External API                    │
│  Business Logic → [BOUNDARY] → Database                     │
│  Core → [BOUNDARY] → Third-party Library                    │
│  Parent Component → [BOUNDARY] → Child Component            │
└─────────────────────────────────────────────────────────────┘

戦略的な境界の配置

エントリー境界:HTTP コントローラー
// ✅ CORRECT: Top-level error middleware
app.use(errorMiddleware);

function errorMiddleware(
  error: Error,
  req: Request,
  res: Response,
  next: NextFunction
) {
  // This is THE boundary between HTTP and application
  
  if (error instanceof ValidationError) {
    return res.status(400).json({ error: error.message, fields: error.fields });
  }
  
  if (error instanceof NotFoundError) {
    return res.status(404).json({ error: error.message });
  }
  
  if (error instanceof UnauthorizedError) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  
  // Log unknown errors, return generic response
  logger.error('Unhandled error', { error, request: req.path });
  return res.status(500).json({ error: 'Internal server error' });
}

// ❌ WRONG: try/catch in every controller
async function getUser(req: Request, res: Response) {
  try {
    const user = await userService.findById(req.params.id);
    res.json(user);
  } catch (error) {
    // Scattered catch - duplicated across all controllers
    res.status(500).json({ error: 'Failed' });
  }
}

// ✅ CORRECT: Let errors propagate to middleware
async function getUser(req: Request, res: Response) {
  const user = await userService.findById(req.params.id);
  res.json(user);
  // Errors propagate to errorMiddleware
}
内部境界:外部サービスアダプター
// ✅ CORRECT: Boundary between your code and external service
class PaymentGatewayAdapter {
  async charge(amount: number, token: string): Promise<ChargeResult> {
    try {
      // External call - this is a boundary
      const response = await this.stripeClient.charges.create({
        amount,
        source: token,
      });
      return this.mapToChargeResult(response);
    } catch (error) {
      // Translate external error to domain error
      if (error instanceof Stripe.CardError) {
        throw new PaymentDeclinedError(error.message, error.code);
      }
      if (error instanceof Stripe.RateLimitError) {
        throw new PaymentServiceUnavailableError('Rate limited');
      }
      if (error instanceof Stripe.APIConnectionError) {
        throw new PaymentServiceUnavailableError('Connection failed');
      }
      throw new PaymentError('Unexpected payment error', { cause: error });
    }
  }
}

// ❌ WRONG: Let Stripe errors leak into business logic
// ❌ WRONG: Catch in OrderService instead of adapter
UI 境界:React エラーバウンダリー
// ✅ CORRECT: Component-level error isolation
class ErrorBoundary extends React.Component<Props, State> {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log to monitoring service
    errorService.report(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <DefaultErrorUI />;
    }
    return this.props.children;
  }
}

// Usage: Isolate features from each other
function App() {
  return (
    <Layout>
      <ErrorBoundary fallback={<DashboardError />}>
        <Dashboard />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<SidebarError />}>
        <Sidebar />
      </ErrorBoundary>
      
      {/* Sidebar error doesn't crash Dashboard */}
    </Layout>
  );
}

境界のチェックリスト

try/catch を追加する前に、次の点を確認してください:

質問いいえの場合……
ここはコンテキストが切り替わるポイントですか?ここでは捕捉しないでください
捕捉することで、意味のある伝播を防げますか?ここでは捕捉しないでください
意味のあるドメインエラーに変換できますか?ここでは捕捉しないでください
具体的な回復処理がありますか?ここでは捕捉しないでください
この境界で責任の所在が変わりますか?ここでは捕捉しないでください

正しい伝播パターン

ビジネスロジック内ではエラーを伝播させます:

// ✅ CORRECT: No random catches in service layer
class OrderService {
  async createOrder(data: OrderData): Promise<Order> {
    // Validate (may throw ValidationError)
    this.validator.validate(data);
    
    // Get user (may throw NotFoundError)
    const user = await this.userRepo.findById(data.userId);
    
    // Check business rules (may throw BusinessRuleError)
    this.rules.assertCanCreateOrder(user, data);
    
    // Process payment (may throw PaymentError from adapter)
    const payment = await this.paymentAdapter.charge(data.amount, user.paymentToken);
    
    // Create order (may throw DatabaseError from repo)
    const order = await this.orderRepo.create({
      ...data,
      paymentId: payment.id,
    });
    
    return order;
    // ALL errors propagate to controller boundary
  }
}

// ❌ WRONG: Defensive try/catch in service
class OrderService {
  async createOrder(data: OrderData): Promise<Order | null> {
    try {
      // ... same logic ...
      return order;
    } catch (error) {
      logger.error('Order creation failed', error);
      return null;  // Lost context, hidden failure
    }
  }
}

境界での適切な縮退運転

境界ではフォールバックを提供できます:

// ✅ CORRECT: Graceful degradation at recommendation boundary
class ProductPage {
  async load(productId: string) {
    // Core data - must succeed
    const product = await this.productService.getById(productId);
    
    // Recommendations - can fail gracefully
    let recommendations: Product[] = [];
    try {
      recommendations = await this.recommendationService.getFor(productId);
    } catch (error) {
      // Log but don't fail the page
      logger.warn('Recommendations unavailable', { productId, error });
      // Empty recommendations is acceptable fallback
    }
    
    return { product, recommendations };
  }
}

重要な違い: これは「必須」機能と「任意」機能の間の境界です。 ランダムな防御的プログラミングではなく、意図的な縮退運転です。

圧力に対抗するための手順

1. 「安全のためにすべてを try/catch で囲む」

圧力:「防御的に、すべてのエラーを捕捉するべきです」

回答: あちこちに捕捉を追加するとバグが隠れ、境界での適切な処理が妨げられます。エラーが伝播するのには理由があります。

対応: 内部の捕捉を削除します。境界でのみ処理します。

2. 「この関数は例外をスローするかもしれない」

圧力:「念のため捕捉しておくべきです」

回答: そのために境界があります。ビジネスロジックはエラー処理を意識すべきではありません。

対応: スローさせます。境界で捕捉します。

3. 「エラーにコンテキストを追加したい」

圧力:「捕捉して情報を追加し、再スローする」

回答: 本当に役立つコンテキストを追加できる場合に限ります。捕捉して再スローするだけでは、たいていノイズが増えるだけです。

対応: そうしなければコンテキストが本当に失われる場合に限り、ラップします。通常は失われません。

4. 「各コンポーネントが自分のエラーを処理するべき」

圧力:「カプセル化とは、ローカルで処理することです」

回答: コンポーネントは適切なエラーをスローするべきです。捕捉するのは境界です。

対応: コンポーネントがスローし、境界が捕捉します。関心事を分離できます。

危険信号 — 停止して再検討してください

次のいずれかに気づいたら、捕捉を削除してください:

  • 純粋なビジネスロジック関数内の try/catch
  • 変換せずに捕捉して再スローする
  • ログを出力して処理を続けるだけの try/catch
  • 空の catch ブロック
  • クラスのすべてのメソッドにある catch
  • 「防御的プログラミング」のための try/catch
  • 意味のある処理ができないエラーの捕捉

これらはすべて、エラーを実際の境界まで伝播させるべきだというサインです。

境界の一覧

アプリケーションの境界を洗い出します:

// Document where boundaries exist
const BOUNDARIES = {
  // Entry points
  HTTP: 'errorMiddleware in app.ts',
  GraphQL: 'formatError in apollo.ts',
  MessageQueue: 'errorHandler in consumer.ts',
  CronJobs: 'wrapWithErrorHandling in scheduler.ts',
  
  // Internal boundaries
  ExternalAPIs: [
    'PaymentGatewayAdapter',
    'EmailServiceAdapter',
    'SearchServiceAdapter',
  ],
  
  // UI boundaries
  React: 'ErrorBoundary components per feature',
  
  // Optional/degradable features
  Degradable: [
    'RecommendationService (fallback: empty)',
    'AnalyticsService (fallback: skip)',
  ],
};

よくある正当化(すべて誤りです)

言い訳実態
「防御的プログラミングは良いことだ」防御的とは入力を検証することです。捕捉をあちこちに散らすことではありません。
「エラーが発生した場所で捕捉する」発生した場所ではスローし、境界で捕捉します。
「捕捉して再スローすればコンテキストを追加できる」たいていノイズが増えるだけです。境界にはコンテキストがあります。
「連鎖的な障害を防ぐ」境界が連鎖を防ぎます。ランダムな捕捉はバグを隠します。
「コンポーネントの独立性」コンポーネントはスローし、境界は捕捉します。それでも独立性は保たれます。
「安全のためのラッパー」ラッパーは障害を隠します。代わりに、失敗は速やかに表面化させます。

クイックリファレンス

場所対応
HTTP ミドルウェア✅ 捕捉してレスポンスに変換
メッセージハンドラー✅ 捕捉し、ack/nack を行ってログに記録
外部アダプター✅ 捕捉してドメインエラーに変換
React エラーバウンダリー✅ 捕捉してフォールバック UI を表示
サービスメソッド❌ エラーを伝播させる
リポジトリメソッド❌ エラーを伝播させる(DB エラーをラップする場合を除く)
ユーティリティ関数❌ エラーを伝播させる
純粋なビジネスロジック❌ エラーを伝播させる

要点

境界はアーキテクチャ上のものです。捕捉は戦略的に行います。

コンテキストが切り替わる箇所(HTTP の入口、外部アダプター、UI コンポーネントの分離、オプション機能の縮退)に try/catch を配置します。ビジネスロジックには決して配置しません。エラーは境界まで伝播させ、そこで適切に変換、記録、処理できるようにします。

コメント