try-catchの正しい使い方と避けるべきアンチパターン

公開日:
目次

エラーハンドリングのために 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チェック
  • 正常なフロー制御
  • エラーが起きる可能性のない処理

エラーハンドリングは「エラーを隠す」ためではなく「エラーに適切に対応する」ためのものです。エラーが発生したことをログに残し、ユーザーには分かりやすいメッセージを表示する。これが基本の考え方になります。

参考

本記事で紹介したアンチパターンやベストプラクティスは、以下の資料でも詳しく解説されています。