AgentSkillsCN

error-handling

错误处理技能。当您需要实现错误处理、与 Sentry 集成,或向用户发送通知时,本技能都能自动触发。它能协助您配置 ErrorBoundary,并运用 AppError 模式,确保应用在异常情况下也能稳定运行。

SKILL.md
--- frontmatter
name: error-handling
description: エラーハンドリングスキル。エラー処理、Sentry連携、ユーザー通知の実装時に自動発動。ErrorBoundary配置とAppErrorパターンを支援。

エラーハンドリングスキル

Dayoptでの統一エラー処理パターンを支援するスキル。

When to Use(自動発動条件)

  • エラー処理を実装する時
  • ErrorBoundaryを配置する時
  • ユーザー向けエラーメッセージを設計する時
  • 「エラー」「error」「Sentry」キーワード

エラー処理の全体像

code
エラー発生
    ↓
AppError に正規化
    ↓
┌─────────────────────────────────────┐
│  1. ログ出力(logger + Sentry)      │
│  2. ユーザー通知(toast/modal)      │
│  3. 自動復旧(retry/fallback)       │
│  4. メトリクス更新                   │
└─────────────────────────────────────┘

グローバルエラーハンドラー

typescript
import { handleError, handleWithRecovery } from '@/lib/error-handler';
import { ERROR_CODES } from '@/config/error-patterns';

// シンプルなエラー処理
try {
  await riskyOperation();
} catch (error) {
  await handleError(error as Error, ERROR_CODES.UNEXPECTED_ERROR, {
    showUserNotification: true,
    source: 'component-name',
  });
}

// 自動復旧付き
const result = await handleWithRecovery(
  () => fetchData(),
  ERROR_CODES.API_TIMEOUT,
  { retryEnabled: true }
);

if (result.success) {
  // 成功(復旧含む)
  console.log(result.data);
} else {
  // 失敗
  console.error(result.error);
}

エラーコード

主要なエラーコードは @/config/error-patterns で定義:

カテゴリコード例用途
AUTHINVALID_TOKEN, SESSION_EXPIRED認証エラー
APIAPI_TIMEOUT, RATE_LIMIT_EXCEEDEDAPIエラー
DATABASECONNECTION_FAILED, DUPLICATE_KEYDBエラー
VALIDATIONREQUIRED_FIELD, INVALID_FORMAT入力エラー

ErrorBoundary配置

配置ルール

code
アプリ全体
└─ Layout ErrorBoundary(致命的エラー用)
   ├─ Feature ErrorBoundary(Plans)
   │  └─ コンポーネント
   ├─ Feature ErrorBoundary(Calendar)
   │  └─ コンポーネント
   └─ Feature ErrorBoundary(Tags)
      └─ コンポーネント

ポイント: 機能単位で分離し、部分的な復旧を可能にする

実装パターン

tsx
// components/ErrorBoundary.tsx
'use client';

import { Component, ReactNode } from 'react';
import { handleError } from '@/lib/error-handler';
import { ERROR_CODES } from '@/config/error-patterns';

interface Props {
  children: ReactNode;
  fallback: ReactNode;
  onError?: (error: Error) => void;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

export class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false, error: null };

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

  componentDidCatch(error: Error) {
    // グローバルエラーハンドラーに報告
    void handleError(error, ERROR_CODES.RENDER_ERROR, {
      source: 'ErrorBoundary',
      showUserNotification: false,
    });

    this.props.onError?.(error);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}
tsx
// 使用例
<ErrorBoundary
  fallback={<ErrorFallback onRetry={() => window.location.reload()} />}
>
  <TagList />
</ErrorBoundary>

ErrorFallback コンポーネント

tsx
interface ErrorFallbackProps {
  title?: string;
  description?: string;
  onRetry?: () => void;
}

export function ErrorFallback({
  title = '問題が発生しました',
  description = 'ページを再読み込みしてください',
  onRetry,
}: ErrorFallbackProps) {
  return (
    <div className="flex flex-col items-center justify-center p-8 text-center">
      <AlertCircle className="h-12 w-12 text-destructive mb-4" />
      <h2 className="text-lg font-semibold">{title}</h2>
      <p className="text-muted-foreground mt-2">{description}</p>
      {onRetry && (
        <Button onClick={onRetry} className="mt-4">
          再試行
        </Button>
      )}
    </div>
  );
}

tRPCエラー → UIエラー変換

typescript
// hooks/useErrorToast.ts
import { TRPCClientError } from '@trpc/client';
import { toast } from 'sonner';

export function useErrorToast() {
  return (error: unknown) => {
    if (error instanceof TRPCClientError) {
      const code = error.data?.code;

      switch (code) {
        case 'UNAUTHORIZED':
          toast.error('ログインが必要です');
          break;
        case 'FORBIDDEN':
          toast.error('権限がありません');
          break;
        case 'NOT_FOUND':
          toast.error('データが見つかりません');
          break;
        case 'BAD_REQUEST':
          toast.error(error.message || '入力内容を確認してください');
          break;
        default:
          toast.error('エラーが発生しました');
      }
    } else {
      toast.error('予期しないエラーが発生しました');
    }
  };
}
typescript
// 使用例
const showErrorToast = useErrorToast();

const mutation = api.tags.create.useMutation({
  onError: showErrorToast,
});

Sentry連携

typescript
// lib/sentry/integration.ts
import * as Sentry from '@sentry/nextjs';
import { AppError } from '@/config/error-patterns';

export function captureAppError(error: AppError) {
  Sentry.captureException(error, {
    tags: {
      category: error.category,
      code: error.code,
      severity: error.severity,
    },
    extra: {
      userMessage: error.userMessage,
      context: error.metadata?.context,
    },
    user: error.metadata?.userId
      ? { id: error.metadata.userId }
      : undefined,
  });
}

// エラー境界と組み合わせ
export function reportErrorToSentry(error: Error, componentStack?: string) {
  Sentry.captureException(error, {
    extra: { componentStack },
  });
}

ユーザー通知パターン

トースト(軽微なエラー)

typescript
toast.error('保存に失敗しました', {
  description: '再試行してください',
  action: {
    label: '再試行',
    onClick: () => retry(),
  },
});

モーダル(重要なエラー)

typescript
// セッション期限切れなど
showErrorModal({
  title: 'セッションが期限切れです',
  description: '再ログインしてください',
  action: {
    label: 'ログイン',
    onClick: () => router.push('/login'),
  },
});

インライン(フォームエラー)

tsx
<FormField
  error={errors.email?.message}
  // ...
/>

チェックリスト

エラー処理実装時:

  • 適切なエラーコードを使用したか
  • ユーザー向けメッセージを設定したか
  • Sentry連携を確認したか
  • 復旧可能なエラーは自動復旧を検討したか

ErrorBoundary配置時:

  • 機能単位で分離したか
  • 適切なfallbackを設定したか
  • 再試行ボタンを提供したか

関連ファイル

code
src/lib/error-handler.ts         # グローバルエラーハンドラー
src/config/error-patterns/       # エラーコード定義
src/lib/sentry/                  # Sentry連携
src/lib/tanstack-query/error-handler.ts  # TanStack Query用

関連スキル

  • /trpc-router-creating - tRPCエラーコード
  • /security - 認証エラー処理
  • /debug - エラーデバッグ