JavaScriptのtry-catchはいつ使う?具体的なユースケースと書き方

はい、承知いたしました。
JavaScriptのtry...catch構文について、具体的なユースケースと書き方を詳細に解説する約5000語の記事を作成します。以下、記事の内容です。


JavaScriptのtry-catchはいつ使う?具体的なユースケースと書き方の完全ガイド

はじめに:なぜエラーハンドリングが重要なのか?

プログラミングにおいて、「エラーは避けて通れないもの」です。どんなに注意深くコードを書いても、予期せぬユーザーの入力、ネットワークの不安定さ、外部APIの仕様変更など、私たちのコントロールが及ばない要因でエラーは発生します。もしこれらのエラーを放置すれば、アプリケーションは突然クラッシュし、ユーザーは真っ白な画面や意味不明なメッセージを前に途方に暮れることになるでしょう。

優れたアプリケーションとそうでないものを分ける重要な要素の一つが、この「予期せぬ事態にどう対処するか」という点、つまりエラーハンドリングです。堅牢で信頼性の高いアプリケーションを構築するためには、エラーが発生することを前提とし、それを優雅に(gracefully)処理する仕組みを組み込む必要があります。

JavaScriptにおいて、このエラーハンドリングの中核を担うのがtry...catch構文です。try...catchは、エラーが発生する可能性のある処理を「試し(try)」、もしエラーが発生したらそれを「捕まえて(catch)」、適切な後処理を行うための強力な仕組みです。

しかし、多くの開発者、特に学習中の人々は、「try...catchが重要であることは知っているが、具体的にいつ、どのように使えば良いのかわからない」という悩みを抱えています。

  • どんなコードをtryブロックで囲むべきなのか?
  • catchブロックでは具体的に何をすれば良いのか?
  • if文でのエラーチェックと何が違うのか?
  • 非同期処理(async/await)ではどう使うのがベストなのか?
  • 逆に、try...catchを「使ってはいけない」ケースはあるのか?

この記事では、これらの疑問に徹底的に答えていきます。try...catchの基本的な構文から、実践的なユースケース、async/awaitとの連携、さらにはカスタムエラーを使った高度なエラーハンドリング戦略まで、網羅的に、そして深く掘り下げて解説します。

この記事を読み終える頃には、あなたはtry...catchを自信を持って使いこなし、より安定した、ユーザーフレンドリーなJavaScriptアプリケーションを構築するための確かな知識を身につけているはずです。

1. try...catch...finallyの基本構文

まずはtry...catch構文の基本的な形と、それぞれのブロックが持つ役割を正確に理解しましょう。

try...catchは、大きく分けて3つのブロックで構成されます。

javascript
try {
// エラーが発生する可能性のあるコード
// このブロック内の処理が実行される
} catch (error) {
// tryブロック内でエラーが発生した場合にのみ実行されるコード
// エラーオブジェクト(error)を使って後処理を行う
} finally {
// エラーの有無にかかわらず、tryまたはcatchの処理が終わった後に必ず実行されるコード
// (このブロックは省略可能)
}

それぞれのブロックを詳しく見ていきましょう。

1.1. tryブロック

tryブロックには、エラーが発生するかもしれない「監視したいコード」を記述します。JavaScriptエンジンはtryブロック内のコードを通常通り実行しようと試みます。

  • エラーが発生しなかった場合: tryブロック内のコードは最後まで実行され、catchブロックはスキップされ、finallyブロック(もしあれば)が実行されます。
  • エラーが発生した場合: その時点でtryブロックの実行は即座に中断され、制御がcatchブロックに移ります。エラー発生行以降のtryブロック内のコードは実行されません。

1.2. catchブロック

catchブロックは、tryブロック内でエラーが発生したときにのみ実行される、いわば「エラー処理の専門家」です。このブロックがなければ、エラーはプログラム全体を停止させてしまいますが、catchブロックがあることで、エラーを捕捉し、アプリケーションのクラッシュを防ぐことができます。

catchブロックは引数を一つ取ります。慣習的にerroreと名付けられることが多いこの引数には、発生したエラーに関する情報が詰まったエラーオブジェクトが渡されます。

エラーオブジェクトの主要なプロパティ

  • name: エラーの種類を示す名前(例: SyntaxError, TypeError, ReferenceError)。
  • message: エラーの具体的な内容を説明する人間が読める形式の文字列。
  • stack: エラーが発生するまでの関数の呼び出し履歴(スタックトレース)。デバッグ時に非常に役立ちます。

簡単な例を見てみましょう。

“`javascript
try {
console.log(‘tryブロックの処理を開始します’);

// 意図的にエラーを発生させる
//存在しない関数を呼び出す
undefinedFunction();

// この行はエラー発生により実行されない
console.log(‘このメッセージは表示されません’);
} catch (error) {
console.error(‘エラーをキャッチしました!’);
console.error(‘エラー名:’, error.name); // ReferenceError
console.error(‘エラーメッセージ:’, error.message); // undefinedFunction is not defined
console.error(‘スタックトレース:’, error.stack);
}

console.log(‘try…catch構文の外の処理は続行されます’);
“`

このコードを実行すると、undefinedFunction()という存在しない関数を呼び出した時点でReferenceErrorが発生します。tryブロックの実行はそこで中断され、制御がcatchブロックに移ります。catchブロックでは、コンソールにエラー情報を出力し、プログラムはクラッシュすることなく最後まで実行されます。

catchの引数省略(ES2019)

エラーオブジェクトの情報が必要ない場合(例えば、特定のエラーが発生したら単に処理を無視したい、など限定的なケース)、引数を省略することも可能です。

javascript
try {
// 何らかの処理
} catch { // 引数なし
// エラーは発生したが、エラー情報は不要な場合の処理
}

1.3. finallyブロック

finallyブロックは、try...catch構文の「後片付け役」です。このブロックに書かれたコードは、tryブロックでエラーが発生したかどうかに関わらず、必ず最後に実行されます

finallyが特に役立つのは、リソースの解放処理です。例えば、ファイルを開いたり、ネットワーク接続を確立したりした場合、処理が成功しようが失敗しようが、最終的にそのリソースを閉じる必要があります。そうしないと、メモリリークや不要な接続が残ってしまう原因になります。

“`javascript
let fileHandle; // ファイルハンドルを保持する変数

try {
console.log(‘ファイルを開きます…’);
// fileHandle = openFile(‘some-file.txt’); // ファイルを開く処理(ダミー)
// ファイルの読み書き処理…
// throw new Error(‘ファイル処理中にエラーが発生しました’); // エラーをシミュレート
} catch (error) {
console.error(‘エラーが発生:’, error.message);
} finally {
console.log(‘finallyブロックが実行されます’);
if (fileHandle) {
// closeFile(fileHandle); // ファイルを閉じる処理
console.log(‘ファイルを閉じました。’);
} else {
console.log(‘ファイルは開かれていませんでした。’);
}
}
“`

この例では、tryブロックでエラーが発生しても発生しなくても、finallyブロックで必ずファイルのクローズ処理が試みられます。これにより、リソースが確実に解放されることが保証されます。

また、trycatchブロック内にreturn文があっても、finallyブロックはそのreturnが実行される直前に実行されます。

“`javascript
function testFinally() {
try {
console.log(‘tryブロック’);
return ‘tryからの戻り値’; // このreturnは一旦保留される
} catch (e) {
console.log(‘catchブロック’);
return ‘catchからの戻り値’;
} finally {
// returnの前に必ず実行される
console.log(‘finallyブロック’);
}
}

console.log(testFinally());
// 出力:
// tryブロック
// finallyブロック
// tryからの戻り値
“`

2. try...catchはいつ使うべきか?- 7つの具体的なユースケース

理論を理解したところで、いよいよ本題です。try...catchを実際にどのような場面で使うべきなのか、具体的なユースケースを通して見ていきましょう。

try...catchを使うべき状況は、一言で言えば「自分ではコントロールできない、実行時(Runtime)に失敗する可能性のある処理」を扱うときです。

ユースケース1:外部APIとの通信(ネットワークリクエスト)

これはtry...catchが最も活躍する典型的なシナリオです。fetch APIやaxiosなどのライブラリを使って外部のサーバーと通信する際、以下のような様々なエラーが発生する可能性があります。

  • サーバーがダウンしている (503 Service Unavailable)
  • リクエストしたリソースが見つからない (404 Not Found)
  • 認証に失敗した (401 Unauthorized)
  • サーバー内部でエラーが発生した (500 Internal Server Error)
  • ユーザーのデバイスがオフラインである(ネットワーク障害)
  • リクエストがタイムアウトした

これらのエラーは、私たちのコードが完璧であっても発生し得ます。try...catchを使えば、これらの状況を優雅に処理し、ユーザーに適切なフィードバックを提供できます。

ここでは、現代の非同期処理で主流のasync/awaitと組み合わせた例を見てみましょう。

“`javascript
// ユーザー情報を取得する関数
async function fetchUserData(userId) {
const loadingIndicator = document.getElementById(‘loading’);
const userDataContainer = document.getElementById(‘user-data’);
const errorMessage = document.getElementById(‘error-message’);

// UIを初期化
loadingIndicator.style.display = ‘block’;
userDataContainer.innerHTML = ”;
errorMessage.textContent = ”;

try {
const response = await fetch(https://api.example.com/users/${userId});

// fetchの注意点: 4xxや5xxのエラーでは例外をスローしない
// そのため、`response.ok`プロパティをチェックする必要がある
if (!response.ok) {
  // サーバーからのエラーレスポンスを元に、自分でエラーを生成してスローする
  throw new Error(`HTTPエラー: ${response.status} ${response.statusText}`);
}

const userData = await response.json();

// 取得したデータでUIを更新
displayUserData(userData);

} catch (error) {
console.error(‘ユーザーデータの取得に失敗しました:’, error);
// ユーザーに分かりやすいエラーメッセージを表示
if (error.name === ‘TypeError’) { // ネットワークエラーなど
errorMessage.textContent = ‘ネットワーク接続を確認してください。’;
} else { // HTTPエラーなど
errorMessage.textContent = ‘データの取得に失敗しました。時間をおいて再度お試しください。’;
}
} finally {
// 成功しても失敗しても、ローディング表示は非表示にする
loadingIndicator.style.display = ‘none’;
}
}

function displayUserData(data) {
const userDataContainer = document.getElementById(‘user-data’);
userDataContainer.innerHTML = <h2>${data.name}</h2>
<p>Email: ${data.email}</p>
;
}
“`

この例のポイント:

  1. fetch自体はネットワークエラー(オフラインなど)でしかPromiseをreject(=エラーをスロー)しません。404や500のようなHTTPエラーステータスは成功として扱います。そのため、response.ok(ステータスコードが200-299の範囲ならtrue)をチェックし、falseならthrow new Error(...)意図的にエラーを発生させcatchブロックに処理を移しています。
  2. catchブロックでは、コンソールに詳細なエラーを記録しつつ、ユーザーには簡潔で分かりやすいメッセージを表示しています。エラーの種類(error.name)によってメッセージを出し分けることも可能です。
  3. finallyブロックを使い、処理の成否にかかわらずローディングインジケータを非表示にしています。これにより、エラー発生時にローディング表示が画面に残り続ける、といった事態を防げます。

ユースケース2:JSONのパース

外部APIからのレスポンスや、ローカルストレージに保存されたデータは、しばしばJSON形式の文字列として扱われます。これをJavaScriptのオブジェクトに変換するためにJSON.parse()を使いますが、この処理は失敗する可能性があります。

  • JSONの文法が間違っている(カンマが抜けている、引用符が閉じていないなど)。
  • そもそもJSON形式ではない文字列が渡された。

このような不正な文字列をJSON.parse()に渡すと、SyntaxErrorが発生します。

“`javascript
function parseUserConfig(jsonString) {
try {
const config = JSON.parse(jsonString);
console.log(‘設定を正常にパースしました:’, config);
return config;
} catch (error) {
if (error instanceof SyntaxError) {
console.error(‘設定データの形式が不正です。デフォルト設定を使用します。’, error.message);
// パースに失敗した場合の代替処理(デフォルト値を返すなど)
return { theme: ‘light’, notifications: false };
} else {
// 予期せぬ別のエラーの場合は、それを再度スローして上位に処理を任せる
throw error;
}
}
}

// 成功するケース
const validJson = ‘{“theme”: “dark”, “notifications”: true}’;
parseUserConfig(validJson);

// 失敗するケース(プロパティ名の”theme”がダブルクォートで囲まれていない)
const invalidJson = ‘{theme: “dark”, “notifications”: true}’;
const defaultConfig = parseUserConfig(invalidJson);
console.log(‘適用された設定:’, defaultConfig);
“`

この例では、JSON.parse()tryで囲むことで、不正なJSONによるアプリケーションのクラッシュを防いでいます。catchブロックでは、instanceof演算子を使ってエラーがSyntaxErrorであることを確認し、その場合は安全なデフォルト設定を返すという復旧処理を行っています。

ユースケース3:ブラウザAPIや外部ライブラリの利用

私たちが利用する機能は、JavaScriptの言語機能だけではありません。ブラウザが提供するWeb API(localStorage, document.querySelectorなど)や、サードパーティ製のライブラリ(React, Vue, jQueryなど)も頻繁に使います。これらの機能が、特定の状況下でエラーをスローすることがあります。

  • localStorageが利用できない環境(プライベートブラウジングモードの一部や、ユーザー設定による無効化)。
  • decodeURIComponent()に不正なエンコード文字列を渡した。
  • 外部ライブラリのメソッドが、予期せぬ引数で呼び出された場合にエラーをスローする。

例えば、localStorageへのアクセスはエラーを発生させる可能性があるため、try...catchで囲むのが定石です。

``javascript
function saveToLocalStorage(key, value) {
try {
const jsonValue = JSON.stringify(value);
localStorage.setItem(key, jsonValue);
console.log(
‘${key}’をローカルストレージに保存しました。`);
} catch (error) {
// 主に2種類のエラーが考えられる
// 1. localStorageが利用不可 (セキュリティ設定など)
// 2. JSON.stringifyが失敗 (循環参照など)
// 3. 容量オーバー (QuotaExceededError)
console.error(‘ローカルストレージへの保存に失敗しました:’, error.name, error.message);
// ユーザーへの通知や、代替の保存方法を検討する
alert(‘設定を保存できませんでした。ブラウザの設定を確認してください。’);
}
}

// 試してみる
saveToLocalStorage(‘userSettings’, { theme: ‘dark’ });
“`

このコードは、localStorage.setItem()が失敗してもアプリケーションが停止しないように保護しています。特に、ユーザーのブラウザ環境に依存する機能は、常に失敗する可能性を考慮に入れるべきです。

ユースケース4:Node.jsでのファイルシステム操作

サーバーサイド(Node.js)で開発を行う場合、ファイルの読み書きは頻繁に行う処理です。fsモジュールを使った操作は、多くの理由で失敗する可能性があります。

  • 指定されたパスにファイルが存在しない。
  • ファイルを読み込む権限(パーミッション)がない。
  • ディスクの空き容量が不足している。

これらの操作をtry...catchで囲むことで、ファイル関連のエラーに堅牢に対応できます。fs/promisesモジュールを使ったasync/awaitの例を見てみましょう。

“`javascript
import { readFile, writeFile } from ‘fs/promises’;

async function processConfigFile(filePath) {
try {
const data = await readFile(filePath, ‘utf8’);
console.log(‘設定ファイルを読み込みました。’);

const config = JSON.parse(data);
config.lastAccessed = new Date().toISOString();

const updatedData = JSON.stringify(config, null, 2);
await writeFile(filePath, updatedData, 'utf8');
console.log('設定ファイルを更新しました。');

} catch (error) {
// エラーの種類によって処理を分岐
if (error.code === ‘ENOENT’) {
console.error(エラー: 設定ファイルが見つかりません (${filePath}));
} else if (error.code === ‘EACCES’) {
console.error(エラー: 設定ファイルへのアクセス権がありません (${filePath}));
} else if (error instanceof SyntaxError) {
console.error(エラー: 設定ファイルのJSON形式が不正です。);
} else {
console.error(‘不明なエラーが発生しました:’, error);
}
// ここでアプリケーションを終了させるか、デフォルト設定で続行するかを決定する
// process.exit(1);
}
}

processConfigFile(‘./config.json’);
“`

この例では、catchブロック内でerror.codeプロパティをチェックしています。Node.jsのファイルシステムエラーは、ENOENT(File Not Found)やEACCES(Permission Denied)といった標準的なエラーコードを持っています。これにより、エラーの原因に応じた具体的なフィードバックや処理の分岐が可能になります。

ユースケース5:必須パラメータや前提条件のチェック

関数やメソッドが正しく動作するために、特定の引数や条件が必須な場合があります。これらの前提条件が満たされていない場合に、処理を中断して呼び出し元に問題を知らせるために、意図的にエラーをスロー(throw)することがあります。そして、その関数の呼び出し側はtry...catchでそれを受け止めることができます。

“`javascript
// ユーザーを分割請求グループに追加する関数
function addUserToBillSplit(user, group) {
// 前提条件のチェック
if (!user) {
throw new Error(‘ユーザーオブジェクトが指定されていません。’);
}
if (!group) {
throw new Error(‘グループオブジェクトが指定されていません。’);
}
if (group.isLocked) {
throw new Error(‘このグループはロックされているため、ユーザーを追加できません。’);
}

// … 正常系の処理 …
group.members.push(user);
console.log(${user.name}さんをグループ「${group.name}」に追加しました。);
}

// 呼び出し側のコード
const currentUser = { name: ‘Alice’ };
const lockedGroup = { name: ‘ディナー会’, members: [], isLocked: true };

try {
addUserToBillSplit(currentUser, lockedGroup);
} catch (error) {
console.error(‘ユーザーの追加に失敗しました。’);
// UIにエラーメッセージを表示
alert(エラー: ${error.message});
}
“`

このパターンは「防御的プログラミング」の一環です。関数内部で不正な状態を検知したら、すぐにthrowで処理を中断し、問題を明確に伝えます。これにより、不正なデータのまま処理が中途半端に進んでしまうことを防ぎ、バグの発見を容易にします。呼び出し側はtry...catchでエラーを受け取り、ユーザーにフィードバックを返すなどの対応ができます。

ユースケース6:データベース操作

データベースとのやり取りも、外部リソースとの連携の一種であり、エラーが発生しやすいポイントです。

  • データベースサーバーへの接続失敗
  • SQLクエリの文法エラー
  • 一意性制約違反(例:同じメールアドレスのユーザーを再度登録しようとした)
  • トランザクションのデッドロック

“`javascript
// (疑似コード)
async function createUserInDatabase(userData) {
let dbClient;
try {
dbClient = await connectToDatabase();
await dbClient.query(‘BEGIN’); // トランザクション開始

const result = await dbClient.query(
  'INSERT INTO users (name, email) VALUES ($1, $2)',
  [userData.name, userData.email]
);

await dbClient.query('COMMIT'); // トランザクション確定
return result.insertedId;

} catch (error) {
if (dbClient) {
await dbClient.query(‘ROLLBACK’); // エラー時はトランザクションをロールバック
console.log(‘トランザクションをロールバックしました。’);
}

// 特定のエラーコード(例: 23505は一意性制約違反)をハンドリング
if (error.code === '23505') {
  throw new Error('このメールアドレスは既に使用されています。');
}

// それ以外のDBエラー
console.error('データベース操作に失敗しました:', error);
throw new Error('データベースエラーが発生しました。しばらくしてから再度お試しください。');

} finally {
if (dbClient) {
await dbClient.release(); // データベース接続を解放
console.log(‘データベース接続を解放しました。’);
}
}
}
“`

この例では、トランザクション処理とエラーハンドリングを組み合わせています。catchブロックでトランザクションをROLLBACKすることで、データが中途半端な状態で保存されることを防いでいます。また、finallyで確実にデータベース接続を解放し、リソースリークを防いでいます。

ユースケース7:非同期処理の並列実行 (Promise.allなど)

複数の非同期処理を並行して実行したい場合、Promise.allが便利です。しかし、Promise.allは、渡されたPromiseのうち一つでもrejectされると、それ自身も即座にrejectします。このエラーを捕捉するためにtry...catchが役立ちます。

“`javascript
async function fetchDashboardData() {
console.log(‘ダッシュボードデータの取得を開始します…’);
try {
const [userData, productData, salesData] = await Promise.all([
fetch(‘https://api.example.com/user/profile’),
fetch(‘https://api.example.com/products/latest’),
fetch(‘https://api.example.com/sales/summary’)
]);

// 全てのレスポンスが成功した場合のみ、この後の処理が実行される
const user = await userData.json();
const products = await productData.json();
const sales = await salesData.json();

console.log('全てのデータの取得に成功しました!');
// UIを更新する処理...

} catch (error) {
// 3つのAPIコールのうち、どれか1つでも失敗するとここに来る
console.error(‘ダッシュボードデータの取得中にエラーが発生しました:’, error);
// ユーザーにエラーがあったことを通知
showErrorUI(‘データの表示に失敗しました。’);
}
}
“`

Promise.allawaitし、全体をtry...catchで囲むことで、複数の非同期処理のいずれかにおける失敗をまとめて一箇所でハンドリングできます。

3. try...catchを使うべきでないケース(アンチパターン)

try...catchは強力なツールですが、銀の弾丸ではありません。誤った使い方をすると、かえってコードを読みにくくし、バグを隠蔽してしまうことさえあります。

アンチパターン1:通常の制御フロー(if文の代わり)としての使用

try...catch例外的な、予期せぬ状況を処理するためのものです。if...elseで処理できるような、プログラムの正常なロジックの一部として使うべきではありません。

悪い例:

javascript
// 配列の要素が存在するかどうかをチェックするためにtry...catchを使っている
function getArrayElement(arr, index) {
try {
const element = arr[index];
if (element === undefined) {
// 厳密にはエラーではないが、エラーを投げてcatchに飛ばす
throw new Error('Element is undefined');
}
return element;
} catch (e) {
return 'デフォルト値';
}
}

このコードは、配列のインデックスが存在しない場合に発生するエラーを期待して、それを制御フローに使っています。これは以下の理由で問題です。

  • 読みにくい: コードの意図が不明確になります。他の開発者は、なぜここで例外処理が使われているのか理解に苦しむでしょう。
  • パフォーマンスが悪い: 例外のスローとキャッチは、単純なif文による条件分岐に比べてはるかにコストが高い処理です。
  • デバッグが困難になる: デバッガが不必要な例外で停止してしまい、本来の問題の発見を妨げる可能性があります。

良い例:

javascript
function getArrayElement(arr, index) {
// if文で境界チェックを行うのが自然で効率的
if (index >= 0 && index < arr.length && arr[index] !== undefined) {
return arr[index];
} else {
return 'デフォルト値';
}
}

アンチパターン2:プログラミングエラー(バグ)の隠蔽

try...catchは、運用時に発生しうる回復可能なエラー(ネットワークエラーなど)を処理するためのものであり、開発中に見つかるべきバグを握りつぶすために使うべきではありません。

非常に悪い例(絶対にやってはいけない):

javascript
// 空のcatchブロックでエラーを完全に無視する
try {
// このコードにはバグがあるかもしれない
let user = getUser();
console.log(user.profile.name); // user.profileがundefinedだとTypeErrorが発生する
} catch (e) {
// 何もしない。エラーを完全に無視する。
// これにより、バグが表面化せず、後の工程でより深刻な問題を引き起こす。
}

TypeError: Cannot read properties of undefinedのようなエラーは、典型的なプログラミング上のミス(バグ)です。これは、user.profileが存在することを前提としているコードが、実際にはundefinedな値を受け取ってしまっていることを示しています。

このようなバグをcatchして何もしないと、アプリケーションはクラッシュこそしませんが、予期せぬ状態で動作を続けることになります。これは、データが破損したり、UIが不整合な状態になったりと、より発見しにくいサイレントなバグの原因となります。

正しい対処法:

  1. バグを修正する: useruser.profileundefinedになりうるのであれば、コードを修正してそのケースを正しく処理します。オプショナルチェイニング (user?.profile?.name) やif文でのチェックが適切です。
  2. エラーを記録する: どうしてもcatchする必要がある場合は、最低でもエラーをコンソールやログサービス(Sentry, Datadogなど)に記録し、開発者がバグの存在を認識できるようにすべきです。

javascript
try {
// ...
} catch (e) {
console.error("予期せぬエラーが発生しました。開発者に報告してください:", e);
// ユーザーには一般的なエラーメッセージを表示する
showGenericErrorToUser();
}

4. 高度なエラーハンドリング戦略

基本的な使い方をマスターしたら、次はより洗練されたエラーハンドリングのテクニックを見ていきましょう。

4.1. カスタムエラーの作成と利用

JavaScriptに組み込まれているError, TypeErrorなどの他に、アプリケーション固有のエラーを表現するために独自のカスタムエラーを作成できます。これにより、エラーハンドリングのロジックをより明確で構造化されたものにできます。

カスタムエラーは、Errorクラスを継承して作ります。

“`javascript
// アプリケーション固有のエラークラスを定義
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = ‘ValidationError’;
}
}

class NetworkError extends Error {
constructor(message, status) {
super(message);
this.name = ‘NetworkError’;
this.status = status; // HTTPステータスコードなどの追加情報を保持
}
}

// APIからユーザーを登録する関数
async function registerUser(email, password) {
// バリデーションチェック
if (!email || !password || password.length < 8) {
throw new ValidationError(‘メールアドレスまたはパスワードが不正です。パスワードは8文字以上である必要があります。’);
}

try {
const response = await fetch(‘/api/register’, { // });
if (!response.ok) {
throw new NetworkError(‘APIリクエストに失敗しました’, response.status);
}
// … 成功時の処理 …
} catch (error) {
// ネットワーク層のエラー(オフラインなど)はここでキャッチされる
// これをより具体的なNetworkErrorにラップして再スローする
throw new NetworkError(‘ネットワークに接続できませんでした’);
}
}

// 呼び出し側でのエラーハンドリング
async function handleRegistration() {
try {
await registerUser(‘[email protected]’, ‘pass’);
} catch (error) {
// instanceofを使ってエラーの種類を判別
if (error instanceof ValidationError) {
// バリデーションエラーの場合の処理
console.warn(‘入力エラー:’, error.message);
// フォームの横にエラーメッセージを表示
showFormError(error.message);
} else if (error instanceof NetworkError) {
// ネットワークエラーの場合の処理
console.error(‘通信エラー:’, error.message, ‘ステータス:’, error.status);
showConnectionError(‘サーバーとの通信に失敗しました。’);
} else {
// その他の予期せぬエラー
console.error(‘不明なエラー:’, error);
showGenericError(‘予期せぬエラーが発生しました。’);
}
}
}
“`

このアプローチの利点:

  • 明確さ: catchブロックのif (error instanceof ...)というコードは、「どんな種類のエラーを処理しようとしているのか」が一目瞭然です。
  • 構造化: エラーの種類ごとに関連するデータを(statusプロパティのように)持たせることができます。
  • 保守性: エラー処理のロジックがエラーの種類ごとに分離されるため、コードの追加や修正が容易になります。

4.2. エラーの再スロー (Re-throwing)

catchブロックは、必ずしもエラー処理の終着点である必要はありません。catchブロック内で、特定の条件下でエラーを処理し、それ以外の処理できないエラーは再度throwすることができます。これをエラーの再スローと呼びます。

これにより、エラーを適切なレベルで処理させることができます。

“`javascript
function doSomethingCritical() {
try {
// 何らかの処理
performTask();
} catch (error) {
// この関数で処理できるのは”TaskSpecificError”だけ
if (error instanceof TaskSpecificError) {
console.log(‘タスク固有のエラーを復旧処理します。’);
// 復旧処理…
} else {
// それ以外のエラー(例: NetworkError, DatabaseError)は、
// この関数では対処できないので、呼び出し元に処理を委譲する
console.error(‘未対応のエラーを上位にスローします。’);
throw error; // エラーを再スロー
}
}
}

// 呼び出し元
try {
doSomethingCritical();
} catch (e) {
// doSomethingCriticalで処理されなかったエラーがここでキャッチされる
console.error(‘上位レベルでエラーをキャッチしました:’, e);
// アプリケーション全体のエラーハンドリングを行う
}
“`

4.3. グローバルなエラーハンドリング

個別のtry...catchで捕捉されなかった「未捕捉の例外(Uncaught Exceptions)」は、最終的にグローバルなレベルで捕捉できます。これは、アプリケーションのクラッシュを防ぐ最後の砦であり、エラーロギングの重要な場所です。

ブラウザ環境:

  • window.onerror: 同期的なコードや古い非同期コード(setTimeoutなど)で発生した未捕捉の例外をキャッチします。
  • window.onunhandledrejection: Promiseでrejectされたにもかかわらず、.catch()try...catchで捕捉されなかった例外をキャッチします。

“`javascript
// ブラウザでのグローバルエラーハンドラ
window.onerror = function(message, source, lineno, colno, error) {
console.log(‘[onerror] 未捕捉の例外をキャッチ:’, error);
// ここでエラーログをサーバーに送信するなどの処理を行う
// logErrorToServer(error);
return true; // trueを返すと、コンソールにエラーが表示されるのを防ぐ
};

window.onunhandledrejection = function(event) {
console.log(‘[onunhandledrejection] 未処理のPromise rejectionをキャッチ:’, event.reason);
// logErrorToServer(event.reason);
event.preventDefault(); // デフォルトのコンソール出力を防ぐ
};
“`

Node.js環境:

  • process.on('uncaughtException', ...)
  • process.on('unhandledRejection', ...)

これらのグローバルハンドラは、あくまで「最後のセーフティネット」です。可能な限り、エラーが発生する可能性のある場所でtry...catchを使って局所的にエラーを処理するべきです。グローバルハンドラに頼りすぎると、どこでエラーが発生したのか追跡するのが難しくなります。

5. try...catchとパフォーマンス

古くから「try...catchはパフォーマンスを低下させる」という話を聞いたことがあるかもしれません。これは、かつてのJavaScriptエンジンでは真実でした。try...catchブロックがあると、エンジンの最適化が一部無効になることがあったためです。

しかし、現代のJavaScriptエンジン(V8など)では、この状況は大きく改善されています

  • tryブロックが存在するだけでは、パフォーマンスへの影響はほとんどありません。コードは通常通り最適化されます。
  • 実際にエラーがスローされ、catchブロックが実行されるときには、パフォーマンスのオーバーヘッドが発生します。これは、スタックトレースの生成など、例外処理のための複雑な内部処理が必要になるためです。

結論として、現代のJavaScript開発において、パフォーマンスを理由にtry...catchの使用をためらう必要は全くありません。エラーが頻繁に発生しない限り(そして、もし頻繁に発生するなら、それは制御フローとして使っているアンチパターンであり、ロジックを見直すべきです)、パフォーマンスへの影響は無視できるレベルです。堅牢性の向上というメリットの方がはるかに大きいのです。

まとめ

try...catchは、JavaScriptで堅牢で信頼性の高いアプリケーションを構築するための、不可欠な言語機能です。それは単なるエラーを隠すための構文ではなく、予期せぬ事態を予測し、それに計画的に対処するための戦略的なツールです。

最後に、この記事で学んだ重要なポイントを振り返りましょう。

  1. 基本を理解する: tryは監視、catchは捕捉と処理、finallyは後片付け。この役割を明確に意識することが第一歩です。
  2. いつ使うかを知る: try...catchは「自分でコントロールできない、実行時に失敗する可能性のある処理」に使います。特に、ネットワーク、ファイルシステム、データベース、外部API、ユーザー入力のパースなどが典型的なユースケースです。
  3. async/awaitと組み合わせる: async/await構文とtry...catchは非常に相性が良く、非同期処理のエラーハンドリングを同期的で直感的なコードで記述できます。
  4. アンチパターンを避ける: if文の代わりに制御フローとして使ったり、プログラミング上のバグを隠蔽するために使ったりしてはいけません。エラーは無視せず、記録し、修正しましょう。
  5. より高度なテクニックを駆使する: カスタムエラーを使ってエラーの種類を明確にし、instanceofで処理を分岐させることで、コードはよりクリーンで保守しやすくなります。
  6. パフォーマンスを恐れない: 現代のエンジンでは、try...catchを適切に使う上でのパフォーマンスコストは問題になりません。

エラーハンドリングは、後から追加する面倒な作業ではなく、機能を実装する最初から設計に組み込むべき重要な要素です。次にコードを書くとき、「この処理が失敗する可能性は?」「失敗した場合、ユーザーに何が起こる?」「どうすれば安全に復旧できる?」と自問自答してみてください。その問いの答えが、あなたをtry...catchへと導き、より優れたソフトウェア開発者へと成長させてくれるはずです。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール