エラーハンドリングの原則
- 早期に失敗する(Fail Fast) - 問題を早期に検出
- 明示的にする - 暗黙のエラーを避ける
- 回復可能性を考慮 - リトライ可能かを判断
- ユーザーに適切なフィードバック - 技術的詳細は隠す
エラーの分類
| 種類 | 例 | 対処法 |
|---|---|---|
| 予期されるエラー | 入力バリデーション失敗 | ユーザーにフィードバック |
| 回復可能なエラー | ネットワークタイムアウト | リトライ |
| 致命的エラー | メモリ不足 | ログ出力して終了 |
| プログラムエラー | null参照 | バグ修正 |
Result型パターン
例外を使わず、戻り値でエラーを表現します。
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// 使用例
async function fetchUser(id: string): Promise<Result<User, ApiError>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return {
success: false,
error: new ApiError(response.status, 'User not found'),
};
}
const user = await response.json();
return { success: true, data: user };
} catch (error) {
return {
success: false,
error: new ApiError(500, 'Network error'),
};
}
}
// 呼び出し側
const result = await fetchUser('123');
if (result.success) {
console.log(result.data.name);
} else {
console.error(result.error.message);
}
カスタムエラークラス
// ベースエラー
class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500,
public isOperational: boolean = true
) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
// 具体的なエラー
class ValidationError extends AppError {
constructor(message: string, public fields: Record<string, string>) {
super(message, 'VALIDATION_ERROR', 400);
}
}
class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`, 'NOT_FOUND', 404);
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Authentication required') {
super(message, 'UNAUTHORIZED', 401);
}
}
// 使用
throw new ValidationError('Invalid input', {
email: 'Invalid email format',
password: 'Too short',
});
グローバルエラーハンドラー
// Express.js
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
// ログ出力
logger.error({
err,
requestId: req.headers['x-request-id'],
path: req.path,
method: req.method,
});
// 運用エラー(予期されるエラー)
if (err instanceof AppError && err.isOperational) {
return res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message,
...(err instanceof ValidationError && { fields: err.fields }),
},
});
}
// プログラムエラー(予期しないエラー)
// 詳細は隠す
return res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
},
});
};
app.use(errorHandler);
Reactエラー境界
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// エラー報告サービスに送信
reportError(error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// 使用
<ErrorBoundary fallback={<ErrorPage />}>
<App />
</ErrorBoundary>
リトライパターン
interface RetryConfig {
maxAttempts: number;
baseDelay: number;
maxDelay: number;
retryableErrors?: string[];
}
async function withRetry<T>(
fn: () => Promise<T>,
config: RetryConfig
): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
// リトライ不可のエラー
if (config.retryableErrors &&
!config.retryableErrors.includes(error.code)) {
throw error;
}
if (attempt === config.maxAttempts) break;
// 指数バックオフ + ジッター
const delay = Math.min(
config.baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000,
config.maxDelay
);
logger.warn({ attempt, delay, error: error.message }, 'Retrying...');
await sleep(delay);
}
}
throw lastError!;
}
// 使用
const data = await withRetry(
() => fetchExternalApi(),
{ maxAttempts: 3, baseDelay: 1000, maxDelay: 10000 }
);
グレースフルデグラデーション
async function getProductWithFallback(id: string): Promise<Product> {
try {
// プライマリソース
return await productService.get(id);
} catch (error) {
logger.warn({ id, error }, 'Primary source failed, trying cache');
try {
// キャッシュフォールバック
const cached = await cache.get(`product:${id}`);
if (cached) return cached;
} catch (cacheError) {
logger.warn({ id }, 'Cache also failed');
}
// 最終フォールバック:デフォルト値
return getDefaultProduct(id);
}
}
関連記事
- ロギングベストプラクティス - エラーログの設計
- サーキットブレーカー - 障害伝播の防止
- Zodバリデーション - 入力検証
まとめ
堅牢なエラーハンドリングは、Result型、カスタムエラークラス、グローバルハンドラー、リトライパターンを組み合わせて実現します。ユーザー体験と運用効率の両方を考慮した設計を心がけましょう。
← Back to list