JavaScriptの基礎 - エラーハンドリング
概要
エラーハンドリングは、プログラムの実行中に発生する予期しない問題を検出・処理するための仕組みである。
JavaScriptでは、try / catch / finally 構文を使用して同期処理のエラーを捕捉し、.catch() や async/await を使用して非同期処理のエラーを処理する。
適切なエラーハンドリングを実装することにより、以下に示すメリットが得られる。
- プログラムのクラッシュ防止
- エラーが発生しても処理を継続できる
- デバッグの効率化
- エラーの原因を特定しやすくなる
- ユーザー体験の向上
- エラー発生時に適切なフィードバックを提供できる
- セキュリティの強化
- 機密情報を含むスタックトレースの漏洩を防止できる
try / catch / finally
try / catch / finally は、JavaScriptにおける同期処理のエラーハンドリングの基本構文である。
catch または finally のどちらかは省略可能だが、両方を同時に省略することはできない。
基本構文
try ブロック内でエラーが発生すると、処理は即座に catch ブロックに移行する。
finally ブロックは、エラーの有無にかかわらず必ず実行される。
try {
// エラーが発生する可能性のある処理
const result = riskyOperation();
console.log(result);
}
catch (error) {
// エラーが発生した場合の処理
console.error('エラーが発生しました:', error.message);
}
finally {
// 成功・失敗にかかわらず常に実行される処理
console.log('処理が完了しました');
}
catchブロック
catch ブロックは、try ブロック内で発生したエラーオブジェクトを引数として受け取る。
ES2019以降、エラーオブジェクトが不要な場合は、括弧を省略した オプショナルcatch binding を使用できる。
// 通常のcatch (エラーオブジェクトを受け取る)
try {
JSON.parse('invalid json');
}
catch (error) {
console.error(error.name); // SyntaxError
console.error(error.message); // エラーメッセージ
}
// オプショナルcatch binding (ES2019以降)
try {
riskyOperation();
}
catch {
console.error('エラーが発生しました');
}
エラーの種類に応じて処理を分岐させることもできる。
try {
processData(input);
}
catch (error) {
if (error instanceof TypeError) {
console.error('型エラー:', error.message);
}
else if (error instanceof RangeError) {
console.error('範囲エラー:', error.message);
}
else {
console.error('予期しないエラー:', error.message);
throw error; // 対処できないエラーは再スロー
}
}
finallyブロック
finally ブロックは、try や catch 内に return 文や throw 文があっても必ず実行される。
ファイルのクローズやDB接続の解放など、リソースの後処理に使用する。
function readFile(filename) {
let fileHandle = null;
try {
fileHandle = openFile(filename);
return fileHandle.read();
}
catch (error) {
console.error('ファイル読み込みエラー:', error.message);
return null;
}
finally {
// return や throw の後でも必ず実行される
if (fileHandle) {
fileHandle.close();
}
}
}
ネストしたtry / catch
try / catch はネストして使用できる。
内側の catch で処理しきれないエラーは、外側の catch に伝播させることができる。
try {
try {
riskyDatabaseCall();
}
catch (dbError) {
// 内側のcatchでエラーをラップして再スロー
throw new Error('Database operation failed', { cause: dbError });
}
}
catch (error) {
// 外側のcatchで最終的なエラー処理
logError(error);
}
Errorオブジェクト
JavaScriptには、エラーの種類ごとに専用の組み込みErrorクラスが用意されている。
全ての組み込みエラーは、Error クラスを継承している。
組み込みErrorの種類
下表に、JavaScriptの主な組み込みエラーの種類を示す。
| エラー型 | 発生条件 | 例 |
|---|---|---|
Error |
汎用エラー | throw new Error('message') |
TypeError |
型が無効 | null.property、'str'.forEach() |
ReferenceError |
未定義変数の参照 | console.log(undefinedVar) |
SyntaxError |
構文エラー | eval("let x =;") |
RangeError |
値が範囲外 | new Array(-1) |
URIError |
無効なURI | decodeURIComponent('%') |
EvalError |
eval関連 (後方互換) | ほぼ使用されない |
AggregateError |
複数エラーの集約 | Promise.any() で全失敗時 |
Errorのプロパティ
下表に、Error オブジェクトの主なプロパティを示す。
| プロパティ | 説明 |
|---|---|
name |
エラー名 (例: "TypeError") |
message |
エラーメッセージ |
stack |
スタックトレース (非標準だがほぼ全環境で利用可能) |
cause |
エラーの原因 (ES2022) |
cause プロパティを使用すると、エラーの根本原因を連鎖的に保持できる。
try {
// 何らかの処理
}
catch (originalError) {
const wrappedError = new Error('処理に失敗しました', { cause: originalError });
console.error(wrappedError.cause); // 元のエラーにアクセス可能
throw wrappedError;
}
throw文
throw 文は、任意の値を例外としてスローするための構文である。
スローされた値は、呼び出しスタックを遡って最初の catch ブロックで捕捉される。
基本構文
throw 文の基本的な使い方を以下に示す。
// 文字列をスロー (非推奨)
throw 'エラーが発生しました';
// Errorオブジェクトをスロー (推奨)
throw new Error('エラーが発生しました');
// 条件に基づいてスロー
function divide(a, b) {
if (b === 0) {
throw new Error('ゼロ除算はできません');
}
return a / b;
}
文字列や数値等、任意の値をスローすることは技術的には可能だが、Error オブジェクトのスローが強く推奨される。
その理由は以下の通りである。
- スタックトレースの自動キャプチャ
- エラー発生箇所の追跡が容易になる。
- 標準プロパティ (name, message, stack) の提供
- エラー情報へのアクセス方法が統一される。
- デバッガやエラーモニタリングツールとの互換性
- Sentry等のツールが正しく認識できる。
Errorオブジェクトのスロー
組み込みエラーを適切に使い分けることで、エラーの原因を明確に伝えることができる。
function processUserInput(input) {
if (typeof input !== 'string') {
throw new TypeError('inputは文字列である必要があります');
}
if (input.length === 0) {
throw new RangeError('inputは1文字以上である必要があります');
}
if (!isKnownVariable(input)) {
throw new ReferenceError(`"${input}" は定義されていません`);
}
return input.trim();
}
try {
processUserInput(42);
}
catch (error) {
console.error(`${error.name}: ${error.message}`);
// TypeError: inputは文字列である必要があります
}
カスタムエラークラス
組み込みエラーに加え、アプリケーション固有のエラーを表現するためにカスタムエラークラスを作成することができる。
カスタムエラークラスは、Error クラスを継承して定義する。
基本的なカスタムエラー
最もシンプルなカスタムエラークラスの定義方法を以下に示す。
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
// 使用例
function validateAge(age) {
if (age < 0 || age > 150) {
throw new ValidationError(`年齢の値が不正です: ${age}`);
}
return age;
}
try {
validateAge(-5);
}
catch (error) {
if (error instanceof ValidationError) {
console.error('バリデーションエラー:', error.message);
}
}
実用的なカスタムエラー
追加プロパティを持つカスタムエラーや、エラー階層の構築例を以下に示す。
// HTTPエラーの基底クラス
class HTTPError extends Error {
constructor(statusCode, message) {
super(message);
this.name = 'HTTPError';
this.statusCode = statusCode;
}
}
// 404 Not Found専用エラー
class NotFoundError extends HTTPError {
constructor(resource) {
super(404, `${resource} not found`);
this.name = 'NotFoundError';
}
}
// 403 Forbidden専用エラー
class ForbiddenError extends HTTPError {
constructor(resource) {
super(403, `Access to ${resource} is forbidden`);
this.name = 'ForbiddenError';
}
}
// 使用例
function fetchResource(path, user) {
if (!resourceExists(path)) {
throw new NotFoundError(path);
}
if (!user.hasPermission(path)) {
throw new ForbiddenError(path);
}
return getResource(path);
}
try {
fetchResource('/admin/data', currentUser);
}
catch (error) {
if (error instanceof NotFoundError) {
res.status(404).send(error.message);
}
else if (error instanceof ForbiddenError) {
res.status(403).send(error.message);
}
else if (error instanceof HTTPError) {
res.status(error.statusCode).send(error.message);
}
else {
res.status(500).send('内部サーバーエラー');
}
}
ES2022の Error.prototype.cause を利用すると、エラーの根本原因を連鎖的に保持できる。
async function connectDatabase(config) {
try {
await db.connect(config);
}
catch (originalError) {
throw new Error('データベース接続に失敗しました', { cause: originalError });
}
}
try {
await connectDatabase(config);
}
catch (error) {
console.error(error.message); // データベース接続に失敗しました
console.error(error.cause.message); // 元のエラーメッセージ
}
非同期処理のエラーハンドリング
非同期処理では、同期処理とは異なるエラーハンドリングの方法が必要になる。
Promiseベースの処理には .catch() メソッドを、async / await には try / catch を使用する。
Promiseのエラー捕捉
Promiseのエラーは、.catch() メソッドで捕捉する。
.then() の第2引数でも捕捉できるが、成功ハンドラ内のエラーを捕捉できないため非推奨である。
// .catch()を使用した方法 (推奨)
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('エラー:', error.message));
// .then()の第2引数を使用した方法 (非推奨)
// 成功ハンドラ (第1引数) 内のエラーを捕捉できない
fetch('/api/data')
.then(
data => console.log(data),
error => console.error('エラー:', error.message)
);
async / awaitのエラー捕捉
async/await を使用した非同期処理では、try / catch でエラーを捕捉できる。
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new HTTPError(response.status, `HTTPエラー: ${response.status}`);
}
return await response.json();
}
catch (error) {
console.error('データの取得に失敗しました:', error.message);
throw error; // 必要に応じて呼び出し元に再スロー
}
}
// 呼び出し側
async function main() {
try {
const user = await fetchUserData(123);
console.log(user);
}
catch (error) {
console.error('ユーザデータを取得できませんでした');
}
}
複数の非同期処理のエラー捕捉
複数の非同期処理を並列実行する場合、Promise.all、Promise.allSettled、Promise.any、Promise.race の違いに注意が必要である。
下表に各メソッドの特性を示す。
| メソッド | 成功条件 | 失敗条件 | 返却値 | 用途 |
|---|---|---|---|---|
Promise.all |
全て成功 | 1つでも失敗 | 結果の配列 | 全て成功が必要な場合 |
Promise.allSettled |
常に完了 | なし | {status, value/reason} の配列 | 全結果を個別確認する場合 |
Promise.any |
1つでも成功 | 全て失敗 | 最初の成功値 | フォールバックが必要な場合 |
Promise.race |
最速の完了 | 最速の完了 | 最初の完了値 | タイムアウト処理 |
各メソッドの使用例を以下に示す。
const p1 = fetch('/api/users');
const p2 = fetch('/api/posts');
const p3 = fetch('/api/comments');
// Promise.all: 1つでも失敗すると全体が失敗
try {
const [users, posts, comments] = await Promise.all([p1, p2, p3]);
}
catch (error) {
console.error('いずれかのリクエストが失敗:', error.message);
}
// Promise.allSettled: 全ての結果を個別に確認できる
const results = await Promise.allSettled([p1, p2, p3]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`リクエスト${index + 1}成功:`, result.value);
}
else {
console.error(`リクエスト${index + 1}失敗:`, result.reason);
}
});
// Promise.any: 最初に成功したものを使用 (全て失敗するとAggregateError)
try {
const data = await Promise.any([
fetch('/api/primary'),
fetch('/api/fallback1'),
fetch('/api/fallback2')
]);
}
catch (error) {
// AggregateError: error.errors に個別のエラーが格納される
console.error('全てのリクエストが失敗:', error.errors);
}
エラーハンドリングのパターン
ガード節パターン
ガード節パターンは、早期リターンを使用して条件のネストを浅くするパターンである。
バリデーションや前提条件のチェックに有効で、ソースコードの可読性が向上する。
ガード節を使用しない場合と使用する場合の比較を以下に示す。
// ガード節なし (深いネスト)
function processUser(user) {
if (user) {
if (user.isActive) {
if (user.email) {
sendEmail(user.email);
return true;
}
else {
console.error('メールアドレスがありません');
return false;
}
}
else {
console.error('ユーザーがアクティブではありません');
return false;
}
}
else {
console.error('ユーザーが存在しません');
return false;
}
}
// ガード節あり (フラットな構造)
function processUser(user) {
if (!user) {
console.error('ユーザーが存在しません');
return false;
}
if (!user.isActive) {
console.error('ユーザーがアクティブではありません');
return false;
}
if (!user.email) {
console.error('メールアドレスがありません');
return false;
}
sendEmail(user.email);
return true;
}
Resultパターン
Resultパターンは、throw を使用せず、成功・失敗を表すオブジェクトを返すパターンである。
{ success, data, error } のような形式で結果を返すことにより、呼び出し側が明示的にエラーを処理するよう強制できる。
// Resultパターンの実装
async function fetchUser(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
return {
success: false,
data: null,
error: `HTTPエラー: ${response.status}`
};
}
const data = await response.json();
return { success: true, data, error: null };
}
catch (error) {
return { success: false, data: null, error: error.message };
}
}
// 呼び出し側
async function main() {
const result = await fetchUser(123);
if (!result.success) {
console.error('取得失敗:', result.error);
return;
}
console.log('取得成功:', result.data);
}
下表に、Resultパターンの主なメリットを示す。
| メリット | 説明 |
|---|---|
| エラーの明示的な処理 | 呼び出し側がエラーチェックを忘れにくい。 |
| 例外の伝播を防止 | 意図しない場所でのエラー処理を避けられる。 |
| TypeScriptとの相性が良い | 型定義によりエラー処理を型安全に行える。 |
関連情報