【メモ】Java の例外と JavaScript、TypeScript の例外はどう違うか

# この記事は何？
JavaとJavaScriptはどちらも例外処理の仕組みを持っていますが、その設計思想や機能には多少の違いがあるようなので整理してメモにしたものです。どなたかのお役に立てば幸いです。

# 1. 型システムと例外オブジェクト

*  **Java**
    *   **厳密な型付け:** Javaの例外はすべて`java.lang.Throwable`クラスのサブクラスでなければなりません。例外はオブジェクトであり、厳密なクラス階層（`Throwable` -> `Error` / `Exception`）を持ちます。
    *   **`Error`:** システムレベルのエラー（例: `OutOfMemoryError`, `StackOverflowError`）で、通常アプリケーションで回復不能な深刻な問題を示します。
    *   **`Exception`:** アプリケーションレベルのエラーで、回復可能な可能性があります。さらに**チェック例外**と**非チェック例外**に分かれます。
*   **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`句でメソッドが送出する可能性のあるチェック例外を宣言します。

```java: Java
    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`構文を使った独自の例外処理パターンがあります。

```javascript: JavaScript
    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`）であることを確認する必要があります。
      *   これにより、ランタイムエラーのリスクを減らしより堅牢なコードを書くことができます。

```typescript: TypeScript
    // 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` を使って、特定のカスタムエラー型を判別し、その型固有のプロパティに安全にアクセスできます。

```typescript: TypeScriptでカスタムエラークラスをスロー
    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()で型ガードを入れる書き方になります。

```typescript: TypeScriptでPromise
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よりも**型安全**なエラー処理コードを書くことを可能にします。カスタムエラークラスも型として扱えるため、より構造化されたコードが実現できます。

各言語はその設計思想に基づいて異なる例外処理のアプローチを採用しています。それぞれの特徴を理解し、開発するアプリケーションの要件やチームのコーディング規約に合わせて適切なエラーハンドリング戦略を選択することが重要です。

以上です。お読みいただきありがとうございました。