error-boundaries について
error-boundaries は、ビジネスロジック全体に捕捉処理を散在させず、意図した境界でエラーを処理する方法を示すソフトウェア設計スキルです。アプリケーションの障害処理やコンポーネントの縮退設計を行う開発者向けです。
主な機能
- 01アーキテクチャ上のエラー境界を特定
- 02捕捉箇所とエラー伝播を案内
- 03境界でのグレースフルデグラデーションを解説
- 04チェックリストと境界の例を提示
活用シーン
- 01HTTP やコマンドライン入口のエラー処理を設計
- 02外部サービスアダプターの障害を分離
- 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 を配置します。ビジネスロジックには決して配置しません。エラーは境界まで伝播させ、そこで適切に変換、記録、処理できるようにします。