🚨 エラーハンドリング
適切なエラーハンドリングは、堅牢なバックエンドサービスを構築するために重要です。Nodeblocks SDKは、すべてのサービス間で一貫性があり、ユーザーフレンドリーなエラーレスポンスを保証する包括的なエラーハンドリングパターンを提供します。
🎯 エラーレスポンス形式
すべてのNodeblocksサービスは一貫したJSON形式でエラーを返します:
{
"error": {
"message": "エラーメッセージの説明",
"data": ["追加のエラー詳細"]
}
}
検証エラー
リクエスト検証が失敗した場合、追加の詳細が含まれます:
{
"error": {
"message": "検証エラー",
"data": [
"リクエストボディには必須プロパティ'name'が必要です",
"リクエストボディには必須プロパティ'email'が必要です",
"リクエストボディに追加プロパティを含めてはいけません"
]
}
}
📋 共通エラーコード
400 Bad Request
- 検証エラー - 無効なリクエストボディ形式または必須フィールドの欠落
- [エンティティ]の作成に失敗しました - データベース挿入操作が挿入IDを返さなかった
- [エンティティ]の更新に失敗しました - 更新操作がデータを変更しない(変更が検出されない)
- [エンティティ]の削除に失敗しました - データベース削除操作が失敗
401 Unauthorized
- トークンを検証できませんでした - 認証トークンが欠落または無効
- 認証に失敗しました - 認証トークンが欠落または無効
- トークンのセキュリティチェックに失敗しました - トークンセキュリティ検証が失敗
403 Forbidden
- ユーザーはこのリソースにアクセスする権限がありません - ユーザーに必要な権限がない
- ユーザーはこの[エンティティ]にアクセスする権限がありません - ユーザーに特定のエンティティ権限がない
404 Not Found
- [エンティティ]が見つかりません - リクエストされた操作でエンティティが存在しない
- [エンティティ]は存在しません - リクエストされた操作でエンティティが存在しない
409 Conflict
- [エンティティ]は既に存在します - 同じ識別子を持つエンティティが既に存在
- このメールアドレスのユーザーは既に存在します - 重複するユーザーメール
500 Internal Server Error
- [エンティティ]の作成に失敗しました - 作成中のデータベース接続問題または予期しない失敗
- [エンティティ]の取得に失敗しました - 取得中のデータベース接続問題または予期しない失敗
- [エンティティ]の検索に失敗しました - 一覧表示中のデータベース接続問題、無効なフィルター構文、または予期しない失敗
- [エンティティ]の更新に失敗しました - 更新中のデータベース接続問題または予期しない失敗
- [エンティティ]の削除に失敗しました - 削除中のデータベース接続問題または予期しない失敗
🔧 サービス固有のエラーパターン
認証サービスエラー
// 無効なログイン認証情報
{
"error": {
"message": "無効なログイン認証情報",
"data": []
}
}
// メール検証が必要
{
"error": {
"message": "メール検証が必要です",
"data": ["アカウントを有効にするためにメールを確認してください"]
}
}
// トークンの有効期限切れ
{
"error": {
"message": "トークンの有効期限が切れています",
"data": ["新しいアクセストークンを取得するためにリフレッシュしてください"]
}
}
ユーザーサービスエラー
// 重複メール
{
"error": {
"message": "このメールアドレスのユーザーは既に存在します",
"data": []
}
}
// ユーザーが見つからない
{
"error": {
"message": "ユーザーが見つかりません",
"data": []
}
}
// 不十分な権限
{
"error": {
"message": "ユーザーはこのリソースにアクセスする権限がありません",
"data": ["管理者権限が必要です"]
}
}
🛠️ エラーハンドリングパターン
Result型を使用したエラーハンドリング
import { ok, err, Result } from 'neverthrow';
import { NodeblocksError } from '@nodeblocks/backend-sdk';
// 成功とエラーを明示的に処理
const createUser = async (userData): Promise<Result<User, NodeblocksError>> => {
// 入力検証
if (!userData.email) {
return err(new NodeblocksError(400, 'メールアドレスが必要です'));
}
try {
// ユーザー作成を試行
const user = await database.users.create(userData);
return ok(user);
} catch (error) {
// データベースエラーをキャッチ
return err(new NodeblocksError(500, 'ユーザーの作成に失敗しました'));
}
};
エラー安全なコンポジション
import { compose, flatMapAsync, lift } from '@nodeblocks/backend-sdk';
// エラーがチェーン全体で自動的に伝播
const userRegistrationPipeline = compose(
validateUserInput, // エラーの場合、以降のステップはスキップ
flatMapAsync(checkEmailUniqueness), // 前のステップが成功の場合のみ実行
flatMapAsync(hashPassword), // 前のステップが成功の場合のみ実行
flatMapAsync(saveUser), // 前のステップが成功の場合のみ実行
flatMapAsync(sendWelcomeEmail), // 前のステップが成功の場合のみ実行
lift(formatSuccessResponse) // 最終レスポンスをフォーマット
);
NodeblocksErrorクラス
import { NodeblocksError } from '@nodeblocks/backend-sdk';
// 基本エラー
const basicError = new NodeblocksError(400, 'リクエストが無効です');
// 追加データ付きエラー
const detailedError = new NodeblocksError(
422,
'検証エラー',
['名前は必須です', 'メールアドレスの形式が無効です']
);
// コンテキスト付きエラー
const contextError = new NodeblocksError(
500,
'データベースエラー',
[],
'createUserHandler'
);
🔄 エラー回復パターン
グレースフルデグラデーション
const getUserWithFallback = async (userId) => {
// プライマリデータソースを試行
const primaryResult = await getUserFromPrimaryDB(userId);
if (primaryResult.isOk()) {
return primaryResult;
}
// フォールバックを試行
const fallbackResult = await getUserFromCache(userId);
if (fallbackResult.isOk()) {
return fallbackResult;
}
// 両方とも失敗した場合
return err(new NodeblocksError(503, 'ユーザーデータが一時的に利用できません'));
};
リトライパターン
const withRetry = async <T>(
operation: () => Promise<Result<T, NodeblocksError>>,
maxAttempts: number = 3,
delay: number = 1000
): Promise<Result<T, NodeblocksError>> => {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const result = await operation();
if (result.isOk()) {
return result;
}
// 最後の試行でない場合は待機
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
return err(new NodeblocksError(500, '最大再試行回数に達しました'));
};
// 使用例
const robustDatabaseOperation = withRetry(
() => saveUserToDatabase(userData),
3, // 3回試行
2000 // 2秒待機
);
サーキットブレーカーパターン
class CircuitBreaker {
private failures = 0;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private lastFailureTime = 0;
constructor(
private threshold: number = 5,
private timeout: number = 60000
) {}
async execute<T>(
operation: () => Promise<Result<T, NodeblocksError>>
): Promise<Result<T, NodeblocksError>> {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime < this.timeout) {
return err(new NodeblocksError(503, 'サービスが一時的に利用できません'));
}
this.state = 'HALF_OPEN';
}
try {
const result = await operation();
if (result.isOk()) {
this.reset();
return result;
} else {
this.recordFailure();
return result;
}
} catch (error) {
this.recordFailure();
return err(new NodeblocksError(500, '操作が失敗しました'));
}
}
private reset() {
this.failures = 0;
this.state = 'CLOSED';
}
private recordFailure() {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.threshold) {
this.state = 'OPEN';
}
}
}
🔍 エラーログとモニタリング
構造化エラーログ
import { nodeblocksLogger } from '@nodeblocks/backend-sdk';
const logError = (error: NodeblocksError, context: any) => {
nodeblocksLogger.error({
event: 'application_error',
error: {
message: error.message,
statusCode: error.statusCode,
data: error.data,
context: error.context
},
requestId: context.requestId,
userId: context.userId,
timestamp: new Date().toISOString()
});
};
// ハンドラーでの使用
export const createUserHandler = async (payload: RouteHandlerPayload) => {
try {
const result = await createUser(payload.params.requestBody);
if (result.isErr()) {
logError(result.error, payload.context);
return result;
}
return result;
} catch (error) {
const nodeError = new NodeblocksError(500, '予期しないエラー');
logError(nodeError, payload.context);
return err(nodeError);
}
};
エラーメトリクス
// エラー率を追跡
const trackErrorMetrics = (error: NodeblocksError, operation: string) => {
// メトリクス収集システムに送信
metrics.increment('errors.total', {
status_code: error.statusCode.toString(),
operation,
error_type: error.context || 'unknown'
});
};
// アラート条件
const alertOnHighErrorRate = (errorRate: number) => {
if (errorRate > 0.05) { // 5%以上のエラー率
sendAlert({
severity: 'high',
message: `エラー率が${errorRate * 100}%に達しました`,
timestamp: new Date().toISOString()
});
}
};
🎯 ベストプラクティス
1. 明示的エラーハンドリング
// 良い - 明示的Result処理
const result = await createUser(userData);
if (result.isErr()) {
return handleError(result.error);
}
const user = result.value;
// 悪い - 例外に依存
try {
const user = await createUser(userData);
} catch (error) {
// エラータイプが不明
}
2. 適切なエラーコンテキスト
// 良い - 詳細なエラー情報
return err(new NodeblocksError(
400,
'ユーザー検証に失敗しました',
['メールアドレスが必要です', '名前は3文字以上である必要があります'],
'validateUserInput'
));
// 悪い - 曖昧なエラー
return err(new NodeblocksError(400, 'エラー'));
3. エラー境界の設定
// サービスレベルでのエラー境界
export const userService = (db, config) => {
return defService(
compose(
withErrorBoundary, // すべてのエラーをキャッチ
userFeatures
)
);
};
const withErrorBoundary = (handler) => async (payload) => {
try {
return await handler(payload);
} catch (error) {
// 予期しないエラーを標準化
const nodeError = error instanceof NodeblocksError
? error
: new NodeblocksError(500, '内部サーバーエラー');
return err(nodeError);
}
};
🔗 関連リソース
- 関数型プログラミング: Result型の詳細
- ハンドラー: エラーハンドリングパターン
- ログ: 構造化ログ
➡️ 次へ
実際のハンドラーでのエラーハンドリングの実装についてハンドラーコンポーネントを参照してください!