𝕏 f B! L
案件・求人数 12,345
案件を探す(準備中) エージェントを探す(準備中) お役立ち情報 ログイン
案件・求人数 12,345
OpenAI Codex CLIでエラーハンドリングを実装する方法|例外設計・リトライ・ログ出力ガイド

OpenAI Codex CLIでエラーハンドリングを実装する方法|例外設計・リトライ・ログ出力ガイド

OpenAI Codex CLIエラーハンドリング例外設計SESエンジニア
目次
⚡ 3秒でわかる!この記事のポイント
  • Codex CLIで堅牢なエラーハンドリングコードを自動生成できる
  • リトライパターン・サーキットブレーカー・DLQまで一貫したエラー設計
  • SES現場で即使えるエラーハンドリングのベストプラクティス集

「try-catchをどこまで細かく書けばいいかわからない」「APIエラーのリトライ処理が複雑すぎる」「ログに何を出力すべきか判断に迷う」

エラーハンドリングは、開発者なら誰もが悩むテーマです。特にSES案件では、既存コードのエラー処理が不十分で障害が頻発するという場面に遭遇することが少なくありません。

OpenAI Codex CLIを使えば、堅牢なエラーハンドリングの設計と実装を大幅に効率化できます。例外クラスの設計からリトライロジック、ログ出力、監視連携まで、品質の高いエラー処理コードを一貫して生成できます。

この記事はOpenAI Codex CLI完全攻略シリーズのEp.27として、エラーハンドリングに特化した実践テクニックを解説します。

この記事でわかること
  • Codex CLIでカスタム例外クラスを設計する方法
  • リトライ・サーキットブレーカーの実装パターン
  • 構造化ログの出力設計
  • API通信のエラーハンドリング
  • 本番環境での監視・アラート連携

Codex CLIでエラーハンドリングを効率化する理由

Codex CLIによるエラーハンドリング設計ワークフロー

エラーハンドリングは「あとでやろう」と後回しにされがちですが、本番環境での障害対応を左右する重要な要素です。Codex CLIを活用すると、プロジェクトのコードベースを分析し、適切なエラー処理パターンを自動で提案・実装してくれます。

Codex CLIが強いポイント

ポイント詳細
既存コードの解析try-catchの漏れやログ不足を検出
パターン適用リトライ・サーキットブレーカー等のパターンを自動実装
一貫性の確保プロジェクト全体で統一されたエラー処理スタイルを維持
テスト生成エラーケースのユニットテストも同時生成

カスタム例外クラスの設計

階層的な例外体系の構築

codex "このプロジェクトに適した例外クラス階層を設計して。
ビジネスロジックエラー、バリデーションエラー、
外部API通信エラー、DB操作エラーを分類して"

Codex CLIが生成する例外体系:

// src/errors/base.ts - 基底エラークラス
export class AppError extends Error {
  public readonly code: string;
  public readonly statusCode: number;
  public readonly isOperational: boolean;
  public readonly context?: Record<string, unknown>;

  constructor(params: {
    message: string;
    code: string;
    statusCode?: number;
    isOperational?: boolean;
    context?: Record<string, unknown>;
    cause?: Error;
  }) {
    super(params.message, { cause: params.cause });
    this.name = this.constructor.name;
    this.code = params.code;
    this.statusCode = params.statusCode ?? 500;
    this.isOperational = params.isOperational ?? true;
    this.context = params.context;

    // V8スタックトレースを正確にキャプチャ
    Error.captureStackTrace(this, this.constructor);
  }
}

// ビジネスロジックエラー
export class BusinessError extends AppError {
  constructor(message: string, code: string, context?: Record<string, unknown>) {
    super({ message, code, statusCode: 422, isOperational: true, context });
  }
}

// バリデーションエラー
export class ValidationError extends AppError {
  public readonly fields: Array<{ field: string; message: string }>;

  constructor(fields: Array<{ field: string; message: string }>) {
    super({
      message: `Validation failed: ${fields.map(f => f.field).join(', ')}`,
      code: 'VALIDATION_ERROR',
      statusCode: 400,
      isOperational: true,
    });
    this.fields = fields;
  }
}

// 外部API通信エラー
export class ExternalApiError extends AppError {
  public readonly service: string;
  public readonly endpoint: string;
  public readonly responseStatus?: number;

  constructor(params: {
    service: string;
    endpoint: string;
    message: string;
    responseStatus?: number;
    cause?: Error;
  }) {
    super({
      message: `[${params.service}] ${params.message}`,
      code: 'EXTERNAL_API_ERROR',
      statusCode: 502,
      isOperational: true,
      context: { service: params.service, endpoint: params.endpoint },
      cause: params.cause,
    });
    this.service = params.service;
    this.endpoint = params.endpoint;
    this.responseStatus = params.responseStatus;
  }
}

// DB操作エラー
export class DatabaseError extends AppError {
  public readonly operation: string;
  public readonly table: string;

  constructor(params: {
    operation: string;
    table: string;
    message: string;
    cause?: Error;
  }) {
    super({
      message: `DB error [${params.table}.${params.operation}]: ${params.message}`,
      code: 'DATABASE_ERROR',
      statusCode: 500,
      isOperational: true,
      context: { operation: params.operation, table: params.table },
      cause: params.cause,
    });
    this.operation = params.operation;
    this.table = params.table;
  }
}

リトライパターンの実装

指数バックオフリトライ

codex "外部API呼び出しの指数バックオフリトライを実装して。
最大3回、ジッターあり、特定のステータスコードのみリトライ対象にして"
// src/utils/retry.ts
interface RetryOptions {
  maxAttempts: number;
  baseDelayMs: number;
  maxDelayMs: number;
  retryableErrors?: (error: unknown) => boolean;
  onRetry?: (attempt: number, error: unknown, delayMs: number) => void;
}

const DEFAULT_OPTIONS: RetryOptions = {
  maxAttempts: 3,
  baseDelayMs: 1000,
  maxDelayMs: 30000,
  retryableErrors: (error) => {
    if (error instanceof ExternalApiError) {
      const status = error.responseStatus;
      // 429 Too Many Requests, 5xx Server Errors のみリトライ
      return status === 429 || (status !== undefined && status >= 500);
    }
    // ネットワークエラー
    if (error instanceof TypeError && error.message.includes('fetch')) {
      return true;
    }
    return false;
  },
};

export async function withRetry<T>(
  fn: () => Promise<T>,
  options: Partial<RetryOptions> = {}
): Promise<T> {
  const opts = { ...DEFAULT_OPTIONS, ...options };
  let lastError: unknown;

  for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      // リトライ対象外ならそのまま投げる
      if (!opts.retryableErrors?.(error)) {
        throw error;
      }

      // 最終試行なら投げる
      if (attempt === opts.maxAttempts) {
        throw error;
      }

      // 指数バックオフ + ジッター
      const exponentialDelay = opts.baseDelayMs * Math.pow(2, attempt - 1);
      const jitter = Math.random() * opts.baseDelayMs;
      const delayMs = Math.min(exponentialDelay + jitter, opts.maxDelayMs);

      opts.onRetry?.(attempt, error, delayMs);

      await new Promise(resolve => setTimeout(resolve, delayMs));
    }
  }

  throw lastError;
}

// 使用例
const user = await withRetry(
  () => fetchUserFromApi(userId),
  {
    maxAttempts: 3,
    onRetry: (attempt, error, delay) => {
      logger.warn(`API retry attempt ${attempt}, waiting ${delay}ms`, { error });
    },
  }
);

サーキットブレーカーパターン

codex "サーキットブレーカーパターンを実装して。
外部APIが連続で失敗したら一定時間リクエストを遮断する仕組みにして"
// src/utils/circuit-breaker.ts
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';

interface CircuitBreakerOptions {
  failureThreshold: number;    // 開放までの失敗回数
  resetTimeoutMs: number;      // OPEN→HALF_OPENまでの時間
  halfOpenMaxAttempts: number;  // HALF_OPENでの試行回数
}

export class CircuitBreaker {
  private state: CircuitState = 'CLOSED';
  private failureCount = 0;
  private lastFailureTime = 0;
  private successCount = 0;

  constructor(
    private readonly name: string,
    private readonly options: CircuitBreakerOptions = {
      failureThreshold: 5,
      resetTimeoutMs: 60000,
      halfOpenMaxAttempts: 3,
    }
  ) {}

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime > this.options.resetTimeoutMs) {
        this.state = 'HALF_OPEN';
        this.successCount = 0;
        logger.info(`Circuit [${this.name}]: OPEN → HALF_OPEN`);
      } else {
        throw new AppError({
          message: `Circuit breaker [${this.name}] is OPEN`,
          code: 'CIRCUIT_OPEN',
          statusCode: 503,
        });
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess() {
    if (this.state === 'HALF_OPEN') {
      this.successCount++;
      if (this.successCount >= this.options.halfOpenMaxAttempts) {
        this.state = 'CLOSED';
        this.failureCount = 0;
        logger.info(`Circuit [${this.name}]: HALF_OPEN → CLOSED`);
      }
    } else {
      this.failureCount = 0;
    }
  }

  private onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    if (this.failureCount >= this.options.failureThreshold) {
      this.state = 'OPEN';
      logger.warn(`Circuit [${this.name}]: CLOSED → OPEN (failures: ${this.failureCount})`);
    }
  }
}

// 使用例
const paymentCircuit = new CircuitBreaker('payment-api');

async function processPayment(order: Order) {
  return paymentCircuit.execute(() =>
    withRetry(() => callPaymentApi(order))
  );
}

構造化ログの設計

ログ出力の統一

codex "このプロジェクトにpinoベースの構造化ログを導入して。
リクエストID、ユーザーID、エラーコンテキストを
自動で含むロガーを作って"
// src/utils/logger.ts
import pino from 'pino';

export const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label }),
  },
  serializers: {
    err: (err) => ({
      type: err.constructor?.name,
      message: err.message,
      code: err.code,
      statusCode: err.statusCode,
      context: err.context,
      stack: process.env.NODE_ENV !== 'production' ? err.stack : undefined,
    }),
  },
  redact: ['req.headers.authorization', 'password', 'token'],
  timestamp: pino.stdTimeFunctions.isoTime,
});

// リクエストスコープのロガー
export function createRequestLogger(requestId: string, userId?: string) {
  return logger.child({
    requestId,
    userId,
    service: process.env.SERVICE_NAME || 'app',
  });
}

// エラーログのヘルパー
export function logError(log: pino.Logger, error: unknown, context?: string) {
  if (error instanceof AppError) {
    if (error.isOperational) {
      log.warn({ err: error, context }, 'Operational error');
    } else {
      log.error({ err: error, context }, 'Programming error');
    }
  } else {
    log.error({ err: error, context }, 'Unexpected error');
  }
}

Expressミドルウェアでのエラー処理

// src/middleware/error-handler.ts
import { Request, Response, NextFunction } from 'express';

export function errorHandler(
  error: Error,
  req: Request,
  res: Response,
  _next: NextFunction
) {
  const log = req.log || logger;

  if (error instanceof ValidationError) {
    log.warn({ err: error }, 'Validation error');
    return res.status(400).json({
      error: {
        code: 'VALIDATION_ERROR',
        message: error.message,
        fields: error.fields,
      },
    });
  }

  if (error instanceof BusinessError) {
    log.warn({ err: error }, 'Business logic error');
    return res.status(422).json({
      error: {
        code: error.code,
        message: error.message,
      },
    });
  }

  if (error instanceof ExternalApiError) {
    log.error({ err: error }, 'External API error');
    return res.status(502).json({
      error: {
        code: 'EXTERNAL_SERVICE_ERROR',
        message: 'External service is temporarily unavailable',
      },
    });
  }

  // 予期しないエラー
  log.error({ err: error }, 'Unhandled error');
  return res.status(500).json({
    error: {
      code: 'INTERNAL_SERVER_ERROR',
      message: 'An unexpected error occurred',
      ...(process.env.NODE_ENV !== 'production' && {
        detail: error.message,
      }),
    },
  });
}

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

プロセスレベルのエラー捕捉

// src/bootstrap/error-handlers.ts
export function setupGlobalErrorHandlers() {
  // 未捕捉の例外
  process.on('uncaughtException', (error: Error) => {
    logger.fatal({ err: error }, 'Uncaught Exception — shutting down');
    // グレースフルシャットダウン
    process.exit(1);
  });

  // 未処理のPromise拒否
  process.on('unhandledRejection', (reason: unknown) => {
    logger.error({ err: reason }, 'Unhandled Rejection');
    // Node.js 15+ではデフォルトでプロセスを停止
    // 監視システムにアラートを送信
    alertService.send({
      severity: 'critical',
      title: 'Unhandled Promise Rejection',
      detail: String(reason),
    });
  });

  // SIGTERMのグレースフルシャットダウン
  process.on('SIGTERM', () => {
    logger.info('SIGTERM received, starting graceful shutdown');
    gracefulShutdown();
  });
}

テストでのエラーハンドリング検証

codex "エラーハンドリングのユニットテストを生成して。
正常系・異常系・境界値を網羅して"
// __tests__/utils/retry.test.ts
describe('withRetry', () => {
  it('成功時は1回で完了する', async () => {
    const fn = vi.fn().mockResolvedValue('success');
    const result = await withRetry(fn);
    expect(result).toBe('success');
    expect(fn).toHaveBeenCalledTimes(1);
  });

  it('リトライ可能なエラーで再試行する', async () => {
    const fn = vi.fn()
      .mockRejectedValueOnce(new ExternalApiError({
        service: 'payment', endpoint: '/charge',
        message: 'Server Error', responseStatus: 500,
      }))
      .mockResolvedValue('success');

    const result = await withRetry(fn, { maxAttempts: 3, baseDelayMs: 10 });
    expect(result).toBe('success');
    expect(fn).toHaveBeenCalledTimes(2);
  });

  it('最大試行回数を超えるとエラーを投げる', async () => {
    const error = new ExternalApiError({
      service: 'payment', endpoint: '/charge',
      message: 'Server Error', responseStatus: 500,
    });
    const fn = vi.fn().mockRejectedValue(error);

    await expect(withRetry(fn, { maxAttempts: 3, baseDelayMs: 10 }))
      .rejects.toThrow('Server Error');
    expect(fn).toHaveBeenCalledTimes(3);
  });

  it('リトライ対象外のエラーは即座に投げる', async () => {
    const error = new ValidationError([{ field: 'email', message: 'Invalid' }]);
    const fn = vi.fn().mockRejectedValue(error);

    await expect(withRetry(fn, { maxAttempts: 3, baseDelayMs: 10 }))
      .rejects.toThrow('Validation failed');
    expect(fn).toHaveBeenCalledTimes(1);
  });
});

SES現場での活用シナリオ

既存コードのエラー処理改善

参画先のプロジェクトでエラー処理が不十分な場合:

codex "このプロジェクトのsrc/以下を分析して、
エラーハンドリングが不足している箇所をリストアップして。
各箇所の修正案も提示して"

Codex CLIが検出する典型的な問題:

1. src/services/user.ts:42 - API呼び出しにtry-catchがない
2. src/controllers/order.ts:87 - catchブロックが空
3. src/utils/db.ts:23 - DBコネクションエラーの処理がない
4. src/middleware/auth.ts:15 - トークン検証の例外が握り潰されている

単価への影響

エラーハンドリングのスキルはSES市場での評価に直結します:

  • 基本的なtry-catch → 一般的なエンジニアの基本スキル
  • リトライパターン・サーキットブレーカー → 月額5〜10万円のスキルアップ
  • 構造化ログ・監視連携 → 月額70〜90万円の案件で必須
  • 障害設計・カオスエンジニアリング → アーキテクト案件で重宝される

まとめ

OpenAI Codex CLIを活用したエラーハンドリングは、コードの堅牢性を飛躍的に向上させます。

この記事のまとめ
  • Codex CLIでカスタム例外クラスの階層設計を自動化
  • 指数バックオフリトライとサーキットブレーカーで外部API連携を堅牢化
  • 構造化ログで障害調査を効率化
  • グローバルエラーハンドラーでプロセスレベルの安全を確保
  • エラーハンドリングスキルはSES高単価案件で差別化要因

OpenAI Codex CLI完全攻略シリーズの他の記事も合わせてご覧ください:

SES案件をお探しですか?

SES記事をもっと読む →
🏗️

SES BASE 編集長

SES業界歴10年以上のメンバーが在籍する編集チーム。SES企業での営業・エンジニア経験、フリーランス独立経験を持つメンバーが、業界のリアルな情報をお届けします。

📊 業界データに基づく記事制作 🔍 IPA・経済産業省データ参照 💼 SES実務経験者が執筆・監修