メインコンテンツまでスキップ

Error Handling

Zeltは、HonoのHTTPExceptionをベースにしたシンプルなエラーハンドリング機構を提供します。

Error Response Format

全てのエラーは一貫したJSON形式で返されます:

{
  "code": "ERROR_CODE",
  "message": "Error description"
}

Built-in Error Types

VALIDATION_FAILED

リクエストボディのバリデーションが失敗したときに返されます(ステータス400):

{
  "code": "VALIDATION_FAILED",
  "issues": [
    {
      "kind": "validation",
      "type": "email",
      "message": "Invalid email",
      "path": ["email"]
    }
  ]
}

INTERNAL_ERROR

未処理のエラーが発生したときに返されます(ステータス500):

{
  "code": "INTERNAL_ERROR",
  "message": "internal server error"
}

開発モード(NODE_ENV=development)では、デバッグのために実際のエラーメッセージが含まれます。

Throwing HTTPExceptions

HonoのHTTPExceptionを使い、ステータスコードとメッセージまたはカスタムレスポンスを指定してHTTPエラーを投げます。

Custom Message

基本的なテキストレスポンスの場合は、エラーのmessageを設定するだけです:

import { HTTPException } from '@zeltjs/core';

throw new HTTPException(401, { message: 'Unauthorized' });

Custom Response

JSONレスポンスやレスポンスヘッダーの設定にはresオプションを使います。

import { HTTPException } from '@zeltjs/core';

const errorResponse = Response.json(
  { code: 'USER_NOT_FOUND', message: 'User not found' },
  { status: 404 }
);

throw new HTTPException(404, { res: errorResponse });

カスタムヘッダー付きの場合:

const errorResponse = new Response('Unauthorized', {
  status: 401,
  headers: {
    'WWW-Authenticate': 'Bearer error="invalid_token"',
  },
});

throw new HTTPException(401, { res: errorResponse });

Cause

デバッグのために元のエラーを付加するにはcauseオプションを使います:

  async use(c: RequestContext, next: Next) {
    try {
      await authorize(c);
    } catch (cause) {
      throw new HTTPException(401, { message: 'Authorization failed', cause });
    }
    await next();
    return undefined;
  }
}

Custom Error Codes

APIの一貫性を保つため、再利用可能なエラーレスポンスを定義します:

import { HTTPException } from '@zeltjs/core';

const notFoundResponse = Response.json(
  { code: 'USER_NOT_FOUND', message: 'User not found' },
  { status: 404 }
);

const forbiddenResponse = Response.json(
  { code: 'FORBIDDEN', message: 'Access denied' },
  { status: 403 }
);

// 使用例
throw new HTTPException(404, { res: notFoundResponse });
throw new HTTPException(403, { res: forbiddenResponse });

または、ファクトリ関数を作成します:

const createErrorResponse = (
  status: number,
  code: string,
  message: string
): Response => {
  return Response.json({ code, message }, { status });
};

// 使用例
const response = createErrorResponse(404, 'USER_NOT_FOUND', 'User not found');
throw new HTTPException(404, { res: response });

Error Types for OpenAPI

組み込みのエラー型を使って、OpenAPI仕様書にエラーレスポンスをドキュメント化します:

import type { ErrorBody, ValidationErrorBody } from '@zeltjs/core';

これらの型はエラーレスポンスの構造を定義します:

  • ErrorBody — 全てのエラー型のUnion(VALIDATION_FAILED | INTERNAL_ERROR)
  • ValidationErrorBody — バリデーションエラー型のみ

Error Handling Flow

Custom Error Handlers

より複雑なエラーハンドリングロジックには、@ErrorHandlerデコレータを使って再利用可能なエラーハンドラクラスを作成します。

Creating an Error Handler

import { ErrorHandler, RequestContext } from '@zeltjs/core';

@ErrorHandler
class DatabaseErrorHandler {
  onError(error: Error, c: RequestContext): Response | undefined {
    if (error.name === 'PrismaClientKnownRequestError') {
      return Response.json(
        { code: 'DATABASE_ERROR', message: 'Database operation failed' },
        { status: 409 }
      );
    }
    return undefined;
  }
}

onErrorメソッドは次を受け取ります:

  • error — 投げられたエラー
  • c — Honoのリクエストcontext

エラーを処理する場合はResponseを返し、次のハンドラへ渡す場合はundefinedを返します。

Registering Error Handlers

errorHandlersオプションを通じて、controllers付きのhttp(...)featureへエラーハンドラを渡します:

const app = createApp([http({
    controllers: [UserController],
    errorHandlers: [DatabaseErrorHandler, ValidationErrorHandler],
  })]);

Handler Chain

エラーハンドラは、http({ errorHandlers: [...] })で登録された順序で実行されます:

  1. 最初のハンドラのonErrorが呼ばれる
  2. undefinedが返された場合、次のハンドラが呼ばれる
  3. 全てのハンドラがundefinedを返した場合、デフォルトのエラーハンドラが実行される
@ErrorHandler
class FirstHandler {
  onError(error: Error, c: RequestContext) {
    if (error instanceof CustomError) {
      return Response.json({ code: 'CUSTOM' }, { status: 400 });
    }
    return undefined;
  }
}

@ErrorHandler
class FallbackHandler {
  onError(error: Error, c: RequestContext) {
    console.error('Unhandled error:', error);
    return undefined;
  }
}

createApp([http({
    controllers: [MyController],
    errorHandlers: [FirstHandler, FallbackHandler],
  })]);

Dependency Injection

エラーハンドラは依存性注入をサポートしています。serviceへアクセスするにはコンストラクタ注入を使います:

@ErrorHandler
class LoggingErrorHandler {
  constructor(private logger = inject(LoggerService)) {}

  onError(error: Error, c: RequestContext) {
    this.logger.error('Request failed', { error, path: c.req.path });
    return undefined;
  }
}

Framework Error Classes

Zeltはフレームワークレベルのエラーのための構造化されたエラークラスを提供します。これらのクラスは一貫した命名規則(Zelt*Error)に従い、デバッグ用の型付きcontextを含みます:

Error ClassDescription
ZeltDecoratorUsageErrorデコレータの不正な使用(例: staticメソッドへの適用)
ZeltLifecycleStateError不正なライフサイクル状態(例: shutdown後のメソッド呼び出し)
ZeltContextNotAvailableError実行context外でのprimitive呼び出し
ZeltAppConfigurationError不正なアプリ設定
ZeltRouteConfigurationError不正なルート設定
ZeltMiddlewareExecutionErrormiddlewareの実行エラー(例: next()の複数回呼び出し)
ZeltNotImplementedErrorメソッドが未実装
ZeltSchemaValidationError不正なschema定義

Usage

import { ZeltAppConfigurationError } from '@zeltjs/core';

try {
  // ...
} catch (error) {
  if (error instanceof ZeltAppConfigurationError) {
    console.log(error.context.reason); // 'no_http_or_commands' | 'duplicate_command'
  }
}

Error Context

各エラークラスは、構造化された情報を持つcontextプロパティを含みます:

// @noErrors
// Reason: 型のみの例でランタイムコードがないため
// ZeltDecoratorUsageErrorのcontext
type DecoratorUsageErrorContext = {
  decoratorName: string;
  reason: 'static_method' | 'missing_decorator';
  targetName?: string;
}

// ZeltLifecycleStateErrorのcontext
type LifecycleStateErrorContext = {
  operation: string;
  currentState: 'disposed' | 'ready' | 'not_ready';
}

Best Practices

  1. 説明的なエラーコードを使うNOT_FOUNDよりUSER_NOT_FOUNDを優先する
  2. 実用的なメッセージを含める — APIの利用者が何が起きたかを理解できるようにする
  3. 内部の詳細を露出させない — 本番環境ではスタックトレースや内部エラーメッセージを含めない
  4. エラーレスポンスをドキュメント化する — OpenAPI schemaを使って全ての起こりうるエラーコードをドキュメント化する
  5. エラーハンドラを具体性の高い順に並べる — 汎用的なハンドラより具体的なハンドラを先に置く
  6. フレームワークのエラーを使うZelt*Errorクラスを捕捉して、フレームワーク固有の問題を処理する