この記事は何?
JavaとJavaScriptはどちらも例外処理の仕組みを持っていますが、その設計思想や機能には多少の違いがあるようなので整理してメモにしたものです。どなたかのお役に立てば幸いです。
1. 型システムと例外オブジェクト
-
Java
-
厳密な型付け: Javaの例外はすべて
java.lang.Throwable
クラスのサブクラスでなければなりません。例外はオブジェクトであり、厳密なクラス階層(Throwable
->Error
/Exception
)を持ちます。 -
Error
: システムレベルのエラー(例:OutOfMemoryError
,StackOverflowError
)で、通常アプリケーションで回復不能な深刻な問題を示します。 -
Exception
: アプリケーションレベルのエラーで、回復可能な可能性があります。さらにチェック例外と非チェック例外に分かれます。
-
厳密な型付け: Javaの例外はすべて
-
JavaScript
-
動的な型付け:
throw
文では任意の型の値(文字列、数値、オブジェクトなど)を投げることができます。 -
Error
オブジェクト推奨: 一般的には、標準組み込みのError
オブジェクト(またはそのサブクラス:SyntaxError
,TypeError
,RangeError
,ReferenceError
など)をthrow
することが推奨されます。これにより、エラーに関する情報(メッセージ、スタックトレースなど)が標準化されます。 - Javaのような厳密な
Error
/Exception
の区別や階層は言語仕様として強制されません。
-
動的な型付け:
2. チェック例外 (Checked Exceptions=別命:検査例外) の有無
-
Java
-
チェック例外が存在:
Exception
クラスのサブクラスのうち、RuntimeException
とそのサブクラス以外のものがチェック例外です(例:IOException
,SQLException
)。 -
コンパイル時チェック: メソッドがチェック例外をスローする可能性がある場合、そのメソッドは
throws
句で宣言するか、try-catch
ブロックで処理しなければなりません。これを怠るとコンパイルエラーになります。これにより、例外処理を忘れにくくする意図があります。 - なお RuntimeException(とそのサブクラス)がなぜ非チェック(非検査)なのかというと、これは主にプログラマのバグが原因で起こるものであり、起きたときはそれをキャッチしてハンドルするよりそのバグを直すべき性質のものだからです。
-
チェック例外が存在:
-
JavaScript
- チェック例外は存在しない: JavaScriptにはコンパイル時に例外処理を強制する仕組みはありません。すべての例外は実行時に発生し、Javaの非チェック例外(Unchecked Exceptions)に近い動作をします。
JavaScriptがJavaのような検査例外という概念を持たないのは、その動的な性質、簡便性と柔軟性を重視する設計思想、他の言語での採用傾向などを総合的に判断した結果とも言えます。エラーハンドリングの責任は、言語(コンパイラ)による強制ではなく、開発者自身に委ねられています。
3. 例外処理の構文
3-1. Javaの例外構文
-
try
,catch
,finally
,throw
,throws
キーワードを使用します。 -
catch
ブロックでは、捕捉したい例外の型を指定できます。複数のcatch
ブロックを型ごとに記述できます。 -
throws
句でメソッドが送出する可能性のあるチェック例外を宣言します。
void readFile(String path) throws IOException { // throws宣言
FileReader reader = null;
try {
reader = new FileReader(path);
// ... ファイル読み込み処理 ...
} catch (FileNotFoundException e) { // 型を指定したcatch
System.err.println("ファイルが見つかりません: " + e.getMessage());
// 特定の例外処理
} catch (IOException e) { // より上位の型もcatch可能
System.err.println("IOエラーが発生しました: " + e.getMessage());
throw e; // 再スローも可能
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.err.println("クローズエラー: " + e.getMessage());
}
}
}
}
3-2. JavaScriptの例外構文
-
try
,catch
,finally
,throw
キーワードを使用します(throws
はありません)。 -
catch
ブロックは通常、型を指定せず、単一のブロックで発生した例外(エラーオブジェクトまたは他の値)を受け取ります。必要であればinstanceof
で型を判定します。 - 非同期処理(Promiseや
async/await
)では、.catch()
メソッドやtry-catch
構文を使った独自の例外処理パターンがあります。
function readFile(path) {
try {
// ... ファイル読み込み処理(同期的な場合) ...
const data = fs.readFileSync(path, 'utf8'); // 例: Node.jsの同期API
if (/* 何らかのエラー条件 */) {
throw new Error("カスタムエラー");
}
console.log("読み込み成功");
} catch (error) { // 型を指定しないcatch
if (error instanceof Error) { // instanceofで型チェック
console.error(`エラーが発生しました: ${error.message}`);
// error.name や error.stack も参照可能
} else {
console.error(`予期せぬ値がスローされました: ${error}`);
}
} finally {
console.log("処理完了(成功または失敗)");
}
}
// 非同期の場合(1) (async/await)
async function readFileAsync(path) {
try {
const data = await fs.promises.readFile(path, 'utf8'); // 例: Node.jsの非同期API
console.log("読み込み成功");
} catch (error) {
console.error(`エラーが発生しました: ${error.message}`);
} finally {
console.log("非同期処理完了");
}
}
// 非同期の場合(2) (Promise.prototype.catch)
function readFilePromise(path) {
fs.promises.readFile(path, 'utf8') // 例: Node.jsの非同期API (Promiseを返す)
.then(data => {
console.log("読み込み成功");
// dataを使った処理など
if (/* 何らかのエラー条件 */) {
// Promiseチェーン内で能動的にエラーを発生させる場合は throw する
// この throw は後続の .catch() で捕捉される
throw new Error("カスタム非同期エラー");
}
return data; // 次の .then に値を渡す
})
// .then(...) さらに処理を続けることも可能
.catch(error => { // Promiseチェーンの途中で発生したエラーを捕捉
console.error(`エラーが発生しました: ${error.message}`);
// ここでエラーに応じた処理を行う
// この .catch() で捕捉された場合、通常は後続の .then() は実行されないが、
// .catch() 内で値を return すれば、回復したとみなされ後続が実行される場合もある
})
.finally(() => { // 成功・失敗に関わらず最後に実行される
console.log("非同期処理完了 (Promise)");
});
}
4. TypeScript ではどうなるんだっけ?
ここでふと、TypeScript だとどうだったか気になってついでに調べました。
TypeScriptはJavaScriptのスーパーセットなので、基本的な例外処理の仕組みはJavaScriptと共通していますが、静的型付けの恩恵を受けることができます。
以下にポイントをまとめます。
4-1. 基本的な構文と動作はJavaScriptと同じ
-
try
,catch
,finally
,throw
キーワードを使用します。 - ランタイムでの例外の挙動はJavaScriptと全く同じです。
- Javaのようなチェック例外(検査例外)の仕組みはありません。すべての例外は実行時に発生します。
-
throw
文では任意の型の値を投げることができますが、JavaScript同様、標準のError
オブジェクト(またはそのサブクラス)をthrow
することが推奨されます。
4-2. catch
ブロックの変数の型
-
歴史的経緯: 以前のTypeScriptでは、
catch
ブロックで捕捉される変数(例:catch (error)
) の型は暗黙的にany
でした。これは型安全性の観点からは問題があり、error
が実際にはError
オブジェクトでない可能性があるのにerror.message
などにアクセスできてしまっていました。 -
TypeScript 4.0 以降 (
unknown
型):-
tsconfig.json
で"useUnknownInCatchVariables": true
を設定する(TypeScript 4.4以降はデフォルトでtrue
になる)と、catch
ブロックの変数の型がunknown
になります。 -
unknown
型はany
型よりも安全です。unknown
型の変数に対してプロパティアクセス(例:error.message
)やメソッド呼び出しを行うには、まず型ガード(instanceof
やtypeof
、ユーザー定義の型ガード関数など)を使って、その変数が実際に期待する型(例:Error
)であることを確認する必要があります。 - これにより、ランタイムエラーのリスクを減らしより堅牢なコードを書くことができます。
-
// tsconfig.json で "useUnknownInCatchVariables": true (推奨) の場合
try {
// 何らかの処理
throw new Error("Something went wrong!");
// throw "ただの文字列"; // これも可能だが非推奨
} catch (error: unknown) { // error の型は unknown
if (error instanceof Error) {
// ここでは error は Error 型であることが保証される
console.error(`Caught an Error object: ${error.message}`);
console.error(error.stack); // stack プロパティにも安全にアクセス可能
} else if (typeof error === 'string') {
console.error(`Caught a string: ${error}`);
} else {
console.error(`Caught an unknown type of error: ${error}`);
}
}
4-3. カスタムエラークラスと型
- TypeScriptでは、独自のカスタムエラークラスを
Error
クラスを継承して定義し、それを型として利用できます。 -
catch
ブロック内でinstanceof
を使って、特定のカスタムエラー型を判別し、その型固有のプロパティに安全にアクセスできます。
class NetworkError extends Error {
constructor(message: string, public statusCode: number) {
super(message);
this.name = 'NetworkError';
}
}
class ValidationError extends Error {
constructor(message: string, public fields: string[]) {
super(message);
this.name = 'ValidationError';
}
}
try {
// API呼び出しなどでエラーが発生したとする
if (/* ネットワークエラー条件 */) {
throw new NetworkError("Failed to fetch data", 500);
}
if (/* バリデーションエラー条件 */) {
throw new ValidationError("Invalid input", ["email", "password"]);
}
} catch (error: unknown) {
if (error instanceof NetworkError) {
console.error(`Network Error: ${error.message}, Status Code: ${error.statusCode}`);
} else if (error instanceof ValidationError) {
console.error(`Validation Error: ${error.message}, Fields: ${error.fields.join(', ')}`);
} else if (error instanceof Error) {
console.error(`Generic Error: ${error.message}`);
} else {
console.error(`Unknown error: ${error}`);
}
}
4-4. TypeScriptで非同期(Promise.prototype.catch)の場合
この場合、基本的な書き方や動作の仕組み(Promiseチェーン、エラーの捕捉)は同じですが、TypeScriptでは型の恩恵と制約があります。
-
書き方の骨格は同じ:
.then().catch().finally()
という流れは変わりません。 -
型の恩恵: TypeScriptでは
.then()
の引数などに型が付き、安全性が向上します。 -
.catch()
の違い: TypeScriptでは.catch()
の引数がunknown
型になるため、型ガードが必須となり、より安全なエラー処理が強制されます。
したがって、上記3-2.で示したJavaScriptのコード例をそのままTypeScriptファイル (.ts
) にコピペした場合.catch(error => ...)
の部分で error
が unknown
型であるため、error.message
に直接アクセスしようとするとコンパイルエラーになります。
TypeScriptで書く場合は、以下のように.catch()で型ガードを入れる書き方になります。
import * as fs from 'fs'; // または import fs from 'fs'; など適切なimport
function readFilePromiseTs(path: string): void {
fs.promises.readFile(path, 'utf8') // Promise<string> を返す
.then((data: string) => { // data は string 型 (推論または明示)
console.log("読み込み成功");
if (/* 何らかのエラー条件 */) {
throw new Error("カスタム非同期エラー");
}
return data;
})
.catch((error: unknown) => { // ★ error は unknown 型
// ★ 型ガードが必須
if (error instanceof Error) {
console.error(`エラーが発生しました: ${error.message}`);
} else {
console.error(`予期せぬ値がスローされました: ${error}`);
}
})
.finally(() => {
console.log("非同期処理完了 (Promise)");
});
}
5. まとめ:Java, JavaScript, TypeScript の例外処理
この記事では、Java、JavaScript、およびTypeScriptにおける例外処理の主な違いを見てきました。それぞれの特徴をまとめると以下のようになります。
特徴 | Java | JavaScript | TypeScript |
---|---|---|---|
例外の型 |
Throwable のサブクラス (オブジェクト) |
任意の型 (通常はError オブジェクト推奨) |
任意の型 (通常はError オブジェクト推奨) |
型システム | 静的型付け (厳密なクラス階層) | 動的型付け (柔軟) | 静的型付け (JavaScriptの挙動 + 型情報) |
チェック例外 |
あり (コンパイル時チェック, throws 必須) |
なし (すべて実行時) | なし (すべて実行時) |
throws 句 |
あり (チェック例外の宣言に必須) | なし | なし |
catch ブロックの変数 |
型指定必須 (複数記述可能) | 型指定なし (instanceof 等で判別) |
unknown 型 (推奨/デフォルト), 型ガード必須 |
非同期処理 | スレッド等で別途考慮が必要 | Promise .catch() , async/await + try-catch
|
Promise .catch() , async/await + try-catch
|
カスタムエラー | クラス継承で定義、catch で型指定可能 |
Error 継承クラス定義可能、instanceof で判別 |
Error 継承クラス定義可能、型として扱える
|
設計思想 | 堅牢性、コンパイル時の安全性 | 柔軟性、簡便性、実行時の処理 | JavaScriptの柔軟性 + 型安全性による堅牢性向上 |
要点:
- Javaは、厳密な型システムとチェック例外により、コンパイル時にエラー処理を強制することで堅牢性を高めています。
-
JavaScriptは、動的な性質とチェック例外の不在により、柔軟性と簡便性を重視しています。
throw
できる値の自由度が高い一方、Error
オブジェクトの使用が推奨されます。非同期処理における例外ハンドリング(.catch()
やasync/await
)が特に重要です。 -
TypeScriptは、JavaScriptの例外処理の仕組みを基本的に継承しつつ、静的型付けの恩恵を加えます。特に、
catch
ブロックの変数をunknown
型として扱い、型ガードを必須とすることで、JavaScriptよりも型安全なエラー処理コードを書くことを可能にします。カスタムエラークラスも型として扱えるため、より構造化されたコードが実現できます。
各言語はその設計思想に基づいて異なる例外処理のアプローチを採用しています。それぞれの特徴を理解し、開発するアプリケーションの要件やチームのコーディング規約に合わせて適切なエラーハンドリング戦略を選択することが重要です。
以上です。お読みいただきありがとうございました。