C
JavaScript/エラー処理/Lesson 18

エラー処理 — try/catch/finally + カスタムエラー

45分·theory

エラー処理 — try/catch/finally + カスタムエラー

🎯 このlessonを読み終えたら

このlessonを読み終えると、以下の3つを自信を持ってできるようになります。

  • ✅ try / catch / finally + Error.cause (ES2022)
  • ✅ カスタムエラークラスによるドメイン例外の分離
  • ✅ async関数のエラー処理 + unhandledrejection

学習目標をチェックリストとして持ち、すべて答えられるようになったらlessonを閉じてください。

try / catch / finally — 基本構造

最もシンプルなtry-catch

javascript
const rawInput = '{ "name": "ホン" }';   // 正しいJSON
// const rawInput = '壊れた文字列';     // ← この行を有効にすると catch が実行される

try {
    const data = JSON.parse(rawInput);   // 💥 失敗するとここで throw
    console.log('成功:', data);          // 成功: { name: 'ホン' }
} catch (err) {
    console.error('パース失敗:', err.message);
    // 失敗時 → "パース失敗: Unexpected token ... in JSON at ..."
}

// 💡 try の中で throw が発生すると → 即座に catch へジャンプ
//    throw がなければ catch はスキップされる

エラーがthrowされると、上のコードは即座にcatchブロックへジャンプします。throwされなければcatchはスキップされます。

finally — 成功・失敗にかかわらず実行

javascript
let conn;
try {
    conn = await db.connect();
    return await conn.query('SELECT ...');
} catch (err) {
    log.error(err);
    throw err;     // 再スロー
} finally {
    if (conn) conn.release();   // クリーンアップ
}

リソースの解放(DBコネクション・ファイル・ロック)の標準パターン。

Errorオブジェクトの4つの情報

javascript
try {
    null.foo;
} catch (e) {
    console.log(e.name);     // 'TypeError'
    console.log(e.message);  // "Cannot read properties of null..."
    console.log(e.stack);    // 呼び出しスタック — *デバッグの核心*
    console.log(e.cause);    // 原因チェイニング (ES2022)
}

よく見るエラーの種類4つ

  • TypeErrornull.fooundefined() — 最もよく遭遇するエラー
  • ReferenceErrorconsle.logのような存在しない変数の参照
  • SyntaxError — 構文そのものが壊れている(通常はビルド段階で検出)
  • RangeError — 配列の長さが負の値・無限再帰

エラーメッセージをそのままAIに見せると、ほとんどの場合原因を指摘してくれます — これが「デバッグのトークン節約」の核心です。

カスタムエラークラス — ドメインごとの分離

なぜカスタムエラーが必要か

すべてのエラーをError一つでthrowすると — catch内で原因ごとの分岐が難しくなります。カスタムクラスで分離しましょう:

javascript
class AppError extends Error {
    constructor(message, code) {
        super(message);
        this.name = 'AppError';
        this.code = code;
    }
}

class NotFoundError extends AppError {
    constructor(resource) {
        super(`${resource} を見つけられません`, 'NOT_FOUND');
        this.statusCode = 404;
    }
}

class ValidationError extends AppError {
    constructor(field) {
        super(`${field} の検証に失敗しました`, 'VALIDATION');
        this.statusCode = 400;
    }
}

分岐処理

javascript
// 🧪 ダミー関数 — 入力に応じて異なるエラーをスロー (コピペ可能)
async function createUser(input) {
    if (!input.email) throw new ValidationError('email');
    if (input.id === 999) throw new NotFoundError('User');
    if (input.crash) throw new Error('DB ダウン');
    return { ok: true };
}

// シミュレーション — Express res オブジェクトを模倣
const res = {
    status: (code) => ({
        json: (body) => console.log(`📤 HTTP ${code}`, body),
        end:  ()     => console.log(`📤 HTTP ${code}`)
    })
};
const log = { error: console.error };

// ▶️ さまざまな入力で分岐を確認
(async () => {
    for (const input of [{ email: '' }, { id: 999, email: 'a' }, { crash: true, email: 'a' }]) {
        try {
            await createUser(input);
        } catch (e) {
            // 🔀 エラーの種類に応じて異なる応答
            if (e instanceof ValidationError) {
                res.status(400).json({ field: e.message });        // 入力エラー
            } else if (e instanceof NotFoundError) {
                res.status(404).end();                              // 見つからない
            } else {
                log.error('Unexpected:', e.message);
                res.status(500).end();                              // サーバーエラー
            }
        }
    }
})();

// 📤 出力:
//   HTTP 400 { field: 'email' }
//   HTTP 404
//   Unexpected: DB ダウン
//   HTTP 500

// 💡 instanceof で「このエラーがどのクラスのインスタンスであるか」を確認
//    → 同じ catch ブロックで種類ごとに異なる処理が可能

instanceofで型による分岐。コードの意図が明確になります。

ES2022 cause — エラーチェーン

javascript
// 🧪 ダミーの低レベル関数 — 常に失敗
async function lowLevelCall() {
    throw new Error('Connection refused');
}

async function loadProduct() {
    try {
        await lowLevelCall();
    } catch (lowLevel) {
        // 🔗 元のエラーを cause として「チェイン」して新しいエラーでラップ
        throw new Error('商品検索失敗', { cause: lowLevel });
    }
}

(async () => {
    try {
        await loadProduct();
    } catch (e) {
        console.error(e.message);         // "商品検索失敗"
        console.error(e.cause.message);   // "Connection refused"   ← 元が残っている!
    }
})();

元のエラーを保持したまま新しいエラーでラップします。ログに元のスタックトレースも残り、デバッグが容易になります。

async / await でのエラー処理 — 標準3パターン

パターン1 — async内のtry/catch

javascript
async function fetchUser(id) {
    try {
        const res = await fetch(`/api/users/${id}`);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return await res.json();
    } catch (err) {
        console.error('失敗:', err);
        return null;
    }
}

最もよく使われる形fetchは4xx/5xxでrejectしませんres.okまたはres.statusを自分で確認する必要があります。

パターン2 — Promise.catch()

javascript
// 🧪 ダミー関数 — 50ms後に失敗
const fetchUser = (id) => new Promise((_, reject) =>
    setTimeout(() => reject(new Error('User not found: ' + id)), 50)
);
const display = (u) => console.log('表示:', u);

fetchUser(1)
    .then(u   => display(u))
    .catch(err => console.error('エラー:', err.message));   // 📤 "エラー: User not found: 1"

async関数でない場所(イベントハンドラなど)で使用。async/awaitと混在させることも可能です。

パターン3 — そのまま上位に投げる

javascript
async function getUser(id) {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();          // エラーは呼び出し元に委任
}

// 呼び出し元が処理
try {
    const u = await getUser(1);
} catch (e) { ... }

ドメインロジックの関数はtry-catchを使いません。責任は最上位の呼び出し元(コントローラ・UIコンポーネント)に委ねます。

よくある落とし穴 — forEach内でのawait

javascript
const items = [1, 2, 3];
const process = (n) => new Promise(r => setTimeout(() => {
    console.log('処理済み:', n);
    r();
}, 100));

// ❌ forEach はコールバックのPromiseを"待たない"
items.forEach(async item => {
    await process(item);
});
console.log('完了');

// 📤 出力順序:
//   完了              ← 🙀 forEach がすぐに終わって先に表示される!
//   処理済み: 1
//   処理済み: 2
//   処理済み: 3

forEachはPromiseを認識しません。for...ofに切り替えましょう:

javascript
const items = [1, 2, 3];
const process = (n) => new Promise(r =>
    setTimeout(() => { console.log('処理:', n); r(); }, 50)
);

(async () => {
    // ✅ 順次 — 1つ終わってから次が始まる (合計150ms)
    console.time('順次');
    for (const item of items) {
        await process(item);
    }
    console.timeEnd('順次');   // 順次: ~150ms

    // ✅ 並列 — 3つ同時に始まる (合計 ~50ms)
    console.time('並列');
    await Promise.all(items.map(item => process(item)));
    console.timeEnd('並列');   // 並列: ~50ms
})();

React Error Boundary — UIエラー処理

jsx
<ErrorBoundary fallback={<div>エラー発生</div>}>
    <MyComponent />
</ErrorBoundary>

レンダリング中のエラーをキャッチしてアプリ全体がクラッシュするのを防ぎますtry-catchはレンダリングエラーを捕捉できません

🤖 AIへのリクエスト例

  • 「このfetchコードにres.okチェックとtry-catchを追加して」
  • 「このエラーをNotFoundError・ValidationErrorに分岐して処理して」
  • 「forEachのawaitが動かないのでfor...ofに変えて」

⚡ 実際に試してみよう — try · catch · finally + カスタムエラー

エラーの種類ごとの分岐処理。instanceofでどのエラーかを判別します。
✏️ JS 코드
📟 コンソール出力
▶ 実行ボタンを押してください
⚠️ ブラウザのサンドボックスで実行 — console.log()のみ対応、alert/fetchは不可
先に読むとよい概念: Async/Await
エラー処理 — try/catch/finally + カスタムエラー - JavaScript