「エラーが起きそうだから、とりあえず全部try-catchで囲んでおけば安心」そんな風に考えていませんか?実は、これがバグを見つけにくくする最大の原因です。広すぎるtry-catch範囲は、エラーを隠蔽し、デバッグを困難にします。
この記事では、なぜ範囲を絞るべきなのか、そして適切なthrowの使い方まで、実際のコード例で解説します。
「とりあえずtry-catch」が生む問題
多くの開発者が陥る、try-catchの典型的な問題パターンを見てみましょう。
広すぎる範囲の弊害
// ❌ 悪い例:範囲が広すぎる
async function processUserData(userData) {
try {
// 100行以上のコードがここに...
const user = validateUser(userData)
const profile = await fetchProfile(user.id)
const preferences = calculatePreferences(profile)
const recommendations = await generateRecommendations(preferences)
const finalData = formatData(recommendations)
await saveToDatabase(finalData)
return finalData
} catch (error) {
console.log('何かエラーが起きました:', error.message)
return null
}
}この書き方の問題点:
- エラーの発生箇所が特定できない
・どの処理でエラーが起きたかわからない
・デバッグに時間がかかる - 異なる種類のエラーが同じ処理される
・バリデーションエラーもDB接続エラーも同じ扱い
・適切な復旧処理ができない - 予期しないエラーも隠蔽される
・プログラムのバグまでキャッチしてしまう
・本来修正すべき問題が見過ごされる
エラーが隠蔽される典型例
// ❌ さらに悪い例:エラーを隠蔽
function calculateTotal(items) {
try {
let total = 0
for (let item of items) {
// typo: item.prie → item.price
total += item.prie * item.quantity
}
return total
} catch (error) {
// エラーをキャッチしても何もしない
return 0 // 無言で0を返す
}
}
// 使用例
const items = [{price: 100, quantity: 2}, {price: 200, quantity: 1}]
const total = calculateTotal(items)
console.log(total) // 0が表示される(本来は400であるべき)この例では、item.prieというtypoによりundefinedの計算が発生しますが、try-catchで隠蔽されてしまい、バグに気づけません。
Before/After:悪い書き方vs良い書き方
同じ機能を実装する場合の比較を見てみましょう。
❌ Before:広範囲なtry-catch
// 悪い例:何でもかんでもtry-catch
async function createUser(userData) {
try {
// バリデーション
if (!userData.email) throw new Error('Email required')
if (!userData.name) throw new Error('Name required')
// 外部API呼び出し
const emailCheck = await checkEmailExists(userData.email)
// データ加工
const processedData = {
email: userData.email.toLowerCase(),
name: userData.name.trim(),
createdAt: new Date()
}
// DB保存
const result = await database.users.create(processedData)
// 通知送信
await sendWelcomeEmail(result.email)
return result
} catch (error) {
// すべてのエラーを同じように処理
console.error('ユーザー作成エラー:', error.message)
throw new Error('ユーザー作成に失敗しました')
}
}✅ After:範囲を絞ったtry-catch
// 良い例:処理ごとに適切にtry-catchを配置
async function createUser(userData) {
// バリデーション(try-catch不要:同期処理で予期できるエラー)
if (!userData.email) {
throw new ValidationError('Email required')
}
if (!userData.name) {
throw new ValidationError('Name required')
}
// 外部API呼び出し(ネットワークエラーのみキャッチ)
let emailExists
try {
emailExists = await checkEmailExists(userData.email)
} catch (error) {
console.error('Email check failed:', error.message)
throw new ExternalServiceError('メール確認サービスが利用できません')
}
if (emailExists) {
throw new ValidationError('このメールアドレスは既に使用されています')
}
// データ加工(try-catch不要:同期処理)
const processedData = {
email: userData.email.toLowerCase(),
name: userData.name.trim(),
createdAt: new Date()
}
// DB保存(DB接続エラーのみキャッチ)
let user
try {
user = await database.users.create(processedData)
} catch (error) {
console.error('Database error:', error.message)
throw new DatabaseError('ユーザー情報の保存に失敗しました')
}
// 通知送信(失敗しても処理を続行)
try {
await sendWelcomeEmail(user.email)
} catch (error) {
console.warn('Welcome email failed:', error.message)
// エラーログは残すが、処理は続行
}
return user
}デバッグしやすさの違い
Before(悪い例)の場合:
- エラーログ: 「ユーザー作成エラー: Network timeout」
- 問題: どこでネットワークタイムアウトが起きたかわからない
- デバッグ: 全体を調査する必要がある
After(良い例)の場合:
- エラーログ: 「Email check failed: Network timeout」
- 問題: メール確認APIでタイムアウトが発生
- デバッグ: 該当部分のみ調査すればよい
try-catch最適化の4原則
効果的なエラーハンドリングのための基本原則を解説します。
① 範囲を最小限に絞る
// ✅ 良い例:エラーが起きる可能性のある処理のみ
async function processFile(filename) {
// ファイル読み込みのみtry-catch
let fileContent
try {
fileContent = await fs.readFile(filename, 'utf8')
} catch (error) {
throw new FileError(`ファイル読み込み失敗: ${filename}`)
}
// 同期処理はtry-catch不要
const lines = fileContent.split('\n')
const filteredLines = lines.filter(line => line.trim() !== '')
// 別の非同期処理は別のtry-catch
try {
await processLines(filteredLines)
} catch (error) {
throw new ProcessingError('行の処理中にエラーが発生')
}
}② エラーの種類別に処理を分ける
// ✅ エラータイプ別の処理
async function fetchUserData(userId) {
try {
const response = await api.get(`/users/${userId}`)
return response.data
} catch (error) {
// HTTPステータスコードで分岐
if (error.response?.status === 404) {
throw new NotFoundError(`ユーザーID ${userId} は存在しません`)
} else if (error.response?.status === 403) {
throw new AuthorizationError('このユーザー情報にアクセスする権限がありません')
} else if (error.response?.status >= 500) {
throw new ServerError('サーバーエラーが発生しました。しばらく待ってから再試行してください')
} else if (error.code === 'NETWORK_ERROR') {
throw new NetworkError('ネットワーク接続を確認してください')
} else {
// 予期しないエラーは詳細情報と共に再throw
console.error('Unexpected API error:', error)
throw new APIError(`API呼び出しエラー: ${error.message}`)
}
}
}③ 適切にthrowで再送出する
// ❌ 悪い例:エラーを握りつぶす
function parseData(jsonString) {
try {
return JSON.parse(jsonString)
} catch (error) {
console.log('Parse error')
return null // 無言でnullを返す(危険)
}
}
// ✅ 良い例:適切にthrowで上位に通知
function parseData(jsonString) {
try {
return JSON.parse(jsonString)
} catch (error) {
console.error('JSON parse failed:', error.message)
// より詳細なエラー情報と共に再throw
throw new ParseError(`無効なJSON形式です: ${error.message}`)
}
}
// ✅ 回復可能な場合のみデフォルト値を返す
function parseDataWithDefault(jsonString, defaultValue = {}) {
try {
return JSON.parse(jsonString)
} catch (error) {
console.warn('JSON parse failed, using default:', error.message)
return defaultValue // 明示的にデフォルト値を返す
}
}④ ログ出力を適切に行う
// ✅ 適切なログレベルと情報
async function uploadFile(file, userId) {
try {
const result = await cloudStorage.upload(file)
console.info('File uploaded successfully:', {
userId,
filename: file.name,
fileId: result.id
})
return result
} catch (error) {
// エラーの詳細情報をログに記録
console.error('File upload failed:', {
userId,
filename: file.name,
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
})
// ユーザー向けにはわかりやすいメッセージで再throw
throw new UploadError('ファイルのアップロードに失敗しました。ファイルサイズや形式を確認してください。')
}
}throw忘れで起きる隠蔽問題
catchしたエラーを適切にthrowしないことで起きる深刻な問題を見てみましょう。
catchしたけど何もしない危険性
// ❌ 危険な例:エラーを隠蔽
async function saveUserSettings(userId, settings) {
try {
await database.updateUserSettings(userId, settings)
} catch (error) {
console.log('設定保存エラー')
// throwしない = エラーが隠蔽される
}
// この処理は実行される(問題!)
console.log('設定を保存しました')
return { success: true } // 嘘の成功レスポンス
}
// 使用側では成功したと思ってしまう
const result = await saveUserSettings(123, {theme: 'dark'})
if (result.success) {
showMessage('設定を保存しました') // 実際は保存されていない
}上位層にエラーを伝える重要性
// ✅ 正しい例:エラーを適切に上位に通知
async function saveUserSettings(userId, settings) {
try {
await database.updateUserSettings(userId, settings)
console.info('User settings saved:', { userId, settings })
return { success: true }
} catch (error) {
console.error('Failed to save user settings:', {
userId,
settings,
error: error.message
})
// 必ずthrowして上位にエラーを通知
throw new SettingsError('設定の保存に失敗しました')
}
}
// 使用側で適切にエラーハンドリング
try {
const result = await saveUserSettings(123, {theme: 'dark'})
showMessage('設定を保存しました')
} catch (error) {
if (error instanceof SettingsError) {
showError('設定の保存に失敗しました。再試行してください。')
} else {
showError('予期しないエラーが発生しました。')
}
}throw vs return の使い分け
// ケース1: 回復不可能なエラー → throw
async function connectToDatabase(config) {
try {
const connection = await createConnection(config)
return connection
} catch (error) {
console.error('Database connection failed:', error.message)
// DB接続失敗は回復不可能 → throw
throw new DatabaseConnectionError('データベースに接続できません')
}
}
// ケース2: 回復可能またはオプション処理 → return
async function fetchUserAvatar(userId) {
try {
const avatar = await api.getUserAvatar(userId)
return avatar
} catch (error) {
console.warn('Avatar fetch failed, using default:', error.message)
// アバター取得失敗は回復可能 → デフォルト値をreturn
return '/images/default-avatar.png'
}
}
// ケース3: 部分的な成功 → 結果オブジェクトでreturn
async function sendNotifications(userIds, message) {
const results = []
for (const userId of userIds) {
try {
await sendNotification(userId, message)
results.push({ userId, success: true })
} catch (error) {
console.error(`Notification failed for user ${userId}:`, error.message)
// 個別の失敗は記録するが処理は続行
results.push({ userId, success: false, error: error.message })
}
}
return results // 成功・失敗の詳細を返す
}言語別実践例
他の言語でも同様の原則が適用できます。
Python例
# ✅ Pythonでの適切なtry-catch
import json
import requests
from typing import Dict, Any
class APIError(Exception):
pass
class ValidationError(Exception):
pass
def fetch_and_process_data(user_id: int) -> Dict[str, Any]:
# バリデーション(try不要)
if not isinstance(user_id, int) or user_id <= 0:
raise ValidationError(f"無効なユーザーID: {user_id}")
# API呼び出し(ネットワークエラーのみキャッチ)
try:
response = requests.get(f"/api/users/{user_id}", timeout=10)
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"API request failed: {e}")
raise APIError(f"ユーザー情報の取得に失敗しました: {e}")
# JSON解析(パースエラーのみキャッチ)
try:
data = response.json()
except json.JSONDecodeError as e:
print(f"JSON decode failed: {e}")
raise APIError("サーバーからの応答が無効です")
# データ処理(try不要:同期処理)
processed_data = {
"id": data.get("id"),
"name": data.get("name", "").strip(),
"email": data.get("email", "").lower()
}
return processed_dataJava例
// ✅ Javaでの適切な例外処理
public class UserService {
public User createUser(UserData userData) throws UserCreationException {
// バリデーション(例外不要)
if (userData.getEmail() == null || userData.getEmail().isEmpty()) {
throw new IllegalArgumentException("メールアドレスは必須です");
}
// DB操作(SQL例外のみキャッチ)
User user;
try {
user = userRepository.save(userData);
} catch (SQLException e) {
logger.error("Database error during user creation", e);
throw new UserCreationException("ユーザー情報の保存に失敗しました", e);
}
// 通知送信(失敗しても処理続行)
try {
emailService.sendWelcomeEmail(user.getEmail());
} catch (EmailException e) {
logger.warn("Welcome email failed for user: " + user.getId(), e);
// エラーログのみ、処理は続行
}
return user;
}
}エラーハンドリングをさらに極める関連記事
try-catchの最適化を習得したら、AI開発支援ツールやコード品質管理ツールも活用してより堅牢なコードを書きましょう:
AI開発支援・コード管理
- Qoder – AIが完全理解するソフトウェア開発向け次世代IDE - プロジェクト全体を理解してエラーハンドリングの改善提案を受ける
- Cipher by Byterover – AIコーディング支援のための共有メモリー管理プラットフォーム - エラーパターンとベストプラクティスの知識を蓄積・共有
- CREAO – AIを活用したカスタムアプリ開発プラットフォーム - 自然言語で堅牢なエラーハンドリングロジックを生成
開発効率化・可視化ツール
- MyLens.ai – アイデアやコンテンツを瞬時にビジュアル化するAI支援ツール - エラーフローや例外処理ロジックの可視化に活用
- FastMoss.com – TikTokショップ分析・運営支援に特化したデータアナリティクスプラットフォーム - エラー分析とパフォーマンス監視の参考に
まとめ:エラーハンドリングは「捕まえる」だけでなく「伝える」
try-catchは「何かにトライして、エラーをキャッチし、適切にスローする」の3段階で考えることが重要です。範囲を絞り、エラーの種類を分け、必要に応じて上位に再送出することで、デバッグしやすく保守性の高いコードになります。
今日から始められるアクション:
- 既存のtry-catchブロックの範囲を見直す
- エラーログに詳細情報を追加する
- throwしていない箇所を特定・修正する
- エラータイプ別の処理分岐を実装する
「とりあえずtry-catch」から卒業して、意図的で効果的なエラーハンドリングを実践しましょう。
try-catch最適化 よくある質問
すべての非同期処理にtry-catchは必要ですか?
いいえ、エラーが起きる可能性のある処理のみに配置すべきです。確実に成功する処理(例:配列のmap操作など)や、上位でまとめてキャッチする設計の場合は不要です。async/awaitを使う際は、Promiseのrejectを適切に処理できる箇所にのみ設置しましょう。
catchでログを出した後、必ずthrowする必要がありますか?
回復不可能なエラーの場合は必ずthrowすべきです。ただし、オプション機能の失敗(アバター画像の取得失敗等)やデフォルト値で代用できる場合は、ログ出力後にデフォルト値をreturnする選択肢もあります。重要なのは呼び出し元が適切に状況を判断できることです。
複数の処理をまとめてtry-catchで囲むのは常に悪いですか?
すべて同じ種類のエラー(例:同一APIへの複数リクエスト)で、同じ処理をする場合は問題ありません。ただし、異なる種類のエラーが混在する場合は分離すべきです。また、どの処理でエラーが起きたかログで識別できるようにしてください。
カスタムエラークラスを作る基準はありますか?
呼び出し元で異なる処理が必要な場合は作成を推奨します。例:ValidationError(ユーザーに入力修正を促す)、NetworkError(再試行を提案)、AuthError(ログイン画面にリダイレクト)など。単純なログ出力のみの場合は、既存のErrorクラスにメッセージを追加するだけで十分です。
