目次
エラーハンドリングのために try-catch を使うことは多いですが、使い方を間違えるとバグを隠してしまったり、デバッグが困難になることがあります。この記事では、try-catchの正しい使い方と、避けるべきアンチパターンを解説します。
try-catchの基本
try-catch は「エラーが発生する可能性のある処理」を安全に実行するための仕組みです。
try {
// エラーが発生するかもしれない処理
const data = JSON.parse(jsonString);
console.log(data);
} catch (error) {
// エラーが発生した場合の処理
console.error('JSONの解析に失敗しました:', error.message);
}
try ブロック内でエラーが発生すると、処理は中断され catch ブロックに移ります。
避けるべきアンチパターン
よくある間違いを紹介します。これらのパターンを知っておくことで、問題のあるコードを書くことを防げます。
アンチパターン1 エラーを黙って握りつぶす
catch ブロックで何もしないパターンです。エラーが発生しても何も起きないように見えるため、本番環境で「なぜかデータが表示されない」という原因特定が難しいバグになります。try-catchをまったく使わない方がエラーが表面化するため、むしろマシと言えます。
// ダメな例
try {
const user = await fetchUser(id);
updateUI(user);
} catch (error) {
// 何もしない
}
// 良い例
try {
const user = await fetchUser(id);
updateUI(user);
} catch (error) {
console.error('ユーザー取得に失敗:', error);
showErrorMessage('データの取得に失敗しました');
}
エラーをログに出力し、ユーザーにも適切なフィードバックを返します。
アンチパターン2 広すぎるスコープでキャッチする
// ダメな例
try {
const config = loadConfig();
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
renderPage(user, posts, comments);
} catch (error) {
console.error('エラーが発生しました');
}
どの処理でエラーが発生したのか分かりません。デバッグが非常に困難になります。
// 良い例
let config, user, posts, comments;
try {
config = loadConfig();
} catch (error) {
console.error('設定の読み込みに失敗:', error);
return;
}
try {
user = await fetchUser(userId);
} catch (error) {
console.error('ユーザー取得に失敗:', error);
showErrorMessage('ユーザー情報を取得できませんでした');
return;
}
// 以下同様に個別にハンドリング
エラーが発生する可能性のある処理ごとに分けることで、問題の特定が容易になります。
アンチパターン3 フロー制御にtry-catchを使う
// ダメな例
function findUser(users, name) {
try {
for (const user of users) {
if (user.name === name) {
throw user; // 見つかったらthrowで返す
}
}
} catch (user) {
return user;
}
return null;
}
throw は例外的な状況を伝えるためのものです。正常なフロー制御に使うとコードが読みにくくなり、パフォーマンスも低下します。
// 良い例
function findUser(users, name) {
return users.find(user => user.name === name) ?? null;
}
アンチパターン4 非同期処理での誤った使い方
// ダメな例
try {
fetch('/api/data').then(response => {
const data = JSON.parse('{ invalid json }'); // ここでエラーが起きてもcatchされない
console.log(data);
});
} catch (error) {
console.error(error); // 到達しない
}
Promise の .then() コールバック内で発生したエラーは、外側の try-catch ではキャッチできません。
// 良い例(async/awaitを使う)
try {
const response = await fetch('/api/data');
const data = await response.json();
} catch (error) {
console.error('データ取得に失敗:', error);
}
// または .catch() を使う
fetch('/api/data')
.then(response => response.json())
.catch(error => console.error('データ取得に失敗:', error));
アンチパターン5 すべてをtry-catchで囲む過剰防御
// ダメな例
function add(a, b) {
try {
return a + b;
} catch (error) {
return 0;
}
}
エラーが起きる可能性のない処理を try-catch で囲むのは無意味です。コードが冗長になり、意図も不明確になります。
try-catchを使うべき場面
では、どのような場面でtry-catchを使うべきでしょうか。
外部とのやり取り
ネットワーク通信、ファイル操作、ユーザー入力の解析など、外部からのデータを扱う場面です。
// ネットワーク通信
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('API呼び出しに失敗:', error);
return null;
}
パース処理
JSONやURLなど、形式が保証されていないデータの解析です。
// JSON解析
function parseJSON(text) {
try {
return JSON.parse(text);
} catch (error) {
console.error('JSONの解析に失敗:', error);
return null;
}
}
サードパーティライブラリの呼び出し
外部ライブラリがどのようなエラーを投げるか分からない場合です。
try {
const result = externalLibrary.process(data);
return result;
} catch (error) {
console.error('外部ライブラリでエラー:', error);
return fallbackProcess(data);
}
エラーを再スローする
キャッチしたエラーを上位に伝える必要がある場合は、再スローします。
async function processOrder(orderId) {
try {
const order = await fetchOrder(orderId);
return await calculateTotal(order);
} catch (error) {
// ログを残してから再スロー
console.error(`注文処理でエラー (ID: ${orderId}):`, error);
throw error; // 呼び出し元に処理を委ねる
}
}
ログを残しつつ、エラーハンドリングの責任を呼び出し元に渡せます。
エラーの種類を区別する
エラーの種類によって処理を分けることもできます。
try {
await saveData(data);
} catch (error) {
if (error.name === 'NetworkError') {
showMessage('ネットワークに接続できません。後で再試行してください。');
} else if (error.name === 'ValidationError') {
showMessage('入力内容に問題があります。');
} else {
console.error('予期しないエラー:', error);
showMessage('エラーが発生しました。');
}
}
使わなくて良い場面
一方で、try-catchを使わなくても対応できる場面もあります。
値のチェックで防げる場合
// try-catchは不要
function getFirstItem(array) {
if (!array || array.length === 0) {
return null;
}
return array[0];
}
事前のチェックで防げるエラーに try-catch は必要ありません。
Optional Chainingで対応できる場合
// try-catchは不要
const city = user?.address?.city ?? '未設定';
nullやundefinedのチェックは ?. と ?? で対応できます。
判断の基準
最後に、try-catchを使うかどうかは以下の基準で判断すると良いと思います。
try-catchを使う場面
- 外部リソース(API、ファイル、データベース)へのアクセス
- 形式が保証されていないデータの解析
- サードパーティライブラリの呼び出し
- ユーザー入力の処理
try-catchを使わない場面
- 事前チェックで防げるエラー
- Optional Chainingで対応できるnullチェック
- 正常なフロー制御
- エラーが起きる可能性のない処理
エラーハンドリングは「エラーを隠す」ためではなく「エラーに適切に対応する」ためのものです。エラーが発生したことをログに残し、ユーザーには分かりやすいメッセージを表示する。これが基本の考え方になります。
参考
本記事で紹介したアンチパターンやベストプラクティスは、以下の資料でも詳しく解説されています。
