本文 AI 產出,尚未審核

JavaScript 錯誤與除錯(Error Handling & Debugging)

主題:throw 機制


簡介

在日常開發中,程式碼不可能永遠「完美」執行。錯誤(Error) 的產生是不可避免的,如何在適當的時機偵測、傳遞與處理錯誤,直接影響到應用程式的穩定性與使用者體驗。
throw 是 JavaScript 中主動拋出例外的關鍵關鍵字,配合 try…catch…finally 可以讓開發者自行決定錯誤的類型、訊息與處理流程。掌握 throw 的使用方式,能夠讓程式碼在面對不可預期情況時,仍保持可讀、可維護,甚至在大型專案中形成統一的錯誤處理策略。

本篇文章將從 概念實作範例常見陷阱最佳實踐 以及 真實應用場景 四個面向,深入探討 throw 機制,並提供可直接套用的程式碼範例,協助初學者與中階開發者快速上手。


核心概念

1. throw 的基本語法

throw expression;
  • expression 可以是任何值:字串、數字、物件、甚至是內建的 Error 物件。
  • throw 被執行時,程式會立即中斷目前的執行流程,並把拋出的值交給最近的 try…catch 區塊(若有)處理;若找不到對應的 catch,錯誤會向上冒泡,最終導致腳本終止。

小提示盡量拋出 Error 或其子類別的實例,而不是純字串或數字,這樣可以保留堆疊資訊(stack trace),有助於除錯。

2. 常見的錯誤類型(Error Sub‑classes)

類別 說明
Error 基礎錯誤類別,所有錯誤皆可從此衍生。
SyntaxError 語法錯誤,通常在解析階段產生。
ReferenceError 變數未定義或無法存取。
TypeError 操作不符合資料型別(如對 null 呼叫方法)。
RangeError 數值超出允許的範圍(如陣列長度負值)。
EvalError eval() 使用不當(已較少見)。
URIError URI 相關函式傳入不合法字串。

實務建議:自訂錯誤類別(繼承自 Error)可讓錯誤更具語意,便於在 catch 中辨別與處理。

3. try…catch…finallythrow 的配合

try {
    // 可能會拋出錯誤的程式碼
} catch (err) {
    // 錯誤處理
} finally {
    // 無論是否有錯誤,都會執行的清理工作
}
  • try:放置可能拋出例外的程式碼。
  • catch:捕捉 throw 或執行階段產生的錯誤,err 參數即為拋出的值。
  • finally:常用於釋放資源(如關閉檔案、清除計時器),即使 catch 內部再次 throwfinally 仍會被執行。

程式碼範例

以下示範 5 個常見且實用的 throw 用法,配合詳細註解說明。

1️⃣ 基本拋出字串(不建議,但可作為概念示範)

function divide(a, b) {
    if (b === 0) {
        // 手動拋出錯誤訊息
        throw "除數不能為 0";
    }
    return a / b;
}

try {
    console.log(divide(10, 0));
} catch (e) {
    console.error("捕捉到錯誤:", e);
}

注意:拋出字串會失去堆疊資訊,除非在極簡情境下才會使用。

2️⃣ 拋出內建 Error 物件

function getUser(id) {
    if (typeof id !== "number") {
        // 拋出 TypeError,讓呼叫者知道參數型別錯誤
        throw new TypeError("id 必須是數字");
    }
    // 假設從資料庫取得使用者
    // ...
    return { id, name: "Alice" };
}

try {
    const user = getUser("123"); // 錯誤的型別
} catch (err) {
    console.error(err.name);    // => TypeError
    console.error(err.message); // => id 必須是數字
    console.error(err.stack);   // 堆疊資訊
}

3️⃣ 自訂錯誤類別(具備額外屬性)

class ValidationError extends Error {
    constructor(message, field) {
        super(message);           // 設定錯誤訊息
        this.name = "ValidationError";
        this.field = field;       // 自訂屬性,指出哪個欄位驗證失敗
    }
}

function validateEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(email)) {
        throw new ValidationError("電子郵件格式不正確", "email");
    }
    return true;
}

try {
    validateEmail("invalid-email");
} catch (err) {
    if (err instanceof ValidationError) {
        console.warn(`欄位 ${err.field} 錯誤:${err.message}`);
    } else {
        console.error(err);
    }
}

4️⃣ 在 catch 中重新拋出(保留原始錯誤)

function parseJSON(jsonStr) {
    try {
        return JSON.parse(jsonStr);
    } catch (originalError) {
        // 包裝成更具語意的錯誤再拋出
        throw new SyntaxError(`JSON 解析失敗:${originalError.message}`);
    }
}

try {
    parseJSON("{ malformed json }");
} catch (e) {
    console.error(e); // SyntaxError: JSON 解析失敗:...
}

5️⃣ 結合 finally 進行資源清理

function readFile(path) {
    let fileHandle = null;
    try {
        fileHandle = openFile(path); // 假設此函式會拋出錯誤
        const data = fileHandle.read();
        return data;
    } catch (err) {
        console.error("讀檔失敗:", err);
        throw err; // 讓上層仍能感知失敗
    } finally {
        if (fileHandle) {
            fileHandle.close(); // 確保檔案一定被關閉
        }
    }
}

常見陷阱與最佳實踐

陷阱 說明 解決方式
拋出非 Error 物件 失去堆疊資訊,難以定位問題。 始終拋出 Error 或其子類別(可自行擴充)。
在非同步環境忘記捕捉 throwPromiseasync/await 中若未 catch,會導致未處理的 rejected。 使用 try…catch 包住 await,或在 Promise 鏈最後加 .catch()
過度使用 throw 每一次 throw 都會中斷執行,過度拋出會降低效能並使程式流程難以追蹤。 僅在真正的異常狀況(非預期、無法自行修復)時使用 throw,否則回傳錯誤資訊或使用 Result 物件。
catch 裏直接 swallow 錯誤 隱藏問題,導致後續 bug 難以偵測。 適當記錄或重新拋出throw err;),除非確定錯誤已被完整處理。
忘記在 finally 釋放資源 可能造成記憶體洩漏、檔案鎖定等問題。 finally必定執行清理程式碼。

最佳實踐清單

  1. 統一錯誤類別:在專案根目錄建立 errors.js,集中管理自訂錯誤。
  2. 錯誤訊息要具可讀性:包含「發生什麼」與「在哪裡」的資訊,避免只寫「錯誤」。
  3. 保留原始錯誤:若要包裝錯誤,使用 cause(ES2022)或自行保存 originalError,方便追蹤。
    throw new Error("高階錯誤訊息", { cause: originalError });
    
  4. 在非同步函式中使用 async/await + try…catch:比起 .then().catch() 更易閱讀。
  5. 記錄錯誤:在 catch 中使用日誌框架(如 winstonpino)寫入檔案或遠端服務,避免僅在 console 顯示。

實際應用場景

1. API 輸入驗證

在後端 Express.js 中,常見做法是先驗證請求參數,若不符合規範即 throw 自訂的 ValidationError,讓全局錯誤處理中介軟體統一回傳 400 錯誤。

// validationMiddleware.js
function validateCreateUser(req, res, next) {
    try {
        const { username, email } = req.body;
        if (!username) throw new ValidationError("缺少 username", "username");
        if (!email)    throw new ValidationError("缺少 email", "email");
        next();
    } catch (err) {
        next(err); // 交給全局 error handler
    }
}

2. 前端表單提交的錯誤回報

在 React 中,使用 async/await 搭配 throw,讓 UI 可以根據不同錯誤類型顯示對應訊息。

async function submitForm(data) {
    try {
        const res = await fetch("/api/register", {
            method: "POST",
            body: JSON.stringify(data),
            headers: { "Content-Type": "application/json" }
        });
        if (!res.ok) {
            const errInfo = await res.json();
            throw new Error(errInfo.message);
        }
        // 成功流程
    } catch (err) {
        // 依錯誤類型顯示不同 UI
        setErrorMessage(err.message);
    }
}

3. 資料庫交易(Transaction)中的錯誤回滾

在 Node.js 使用 sequelizetypeorm 時,若在交易過程中發生任何錯誤,都會 throw,最終在 finally 中自動呼叫 rollback

async function transferFunds(fromId, toId, amount) {
    const transaction = await sequelize.transaction();
    try {
        await Account.decrement('balance', { by: amount, where: { id: fromId }, transaction });
        await Account.increment('balance', { by: amount, where: { id: toId }, transaction });
        await transaction.commit();
    } catch (err) {
        await transaction.rollback(); // 確保資料不會不一致
        throw err; // 讓上層感知失敗
    }
}

總結

  • throw主動拋出例外 的關鍵工具,配合 try…catch…finally 能讓錯誤在程式執行流程中被有序捕捉與處理。
  • 最佳實踐:拋出 Error(或自訂子類別),保留堆疊資訊;在非同步環境中務必使用 try…catch.catch();錯誤訊息要具可讀性且含有上下文。
  • 常見陷阱包括拋出非 Error 物件、忘記在非同步函式中捕捉、以及在 catch 中直接吞掉錯誤。只要遵循前述的 統一錯誤類別、適當記錄、確保資源釋放 原則,就能寫出穩定、易除錯的 JavaScript 程式碼。

掌握 throw 機制後,你將能在 前端 UI、後端 API、資料庫交易 等各種情境中,建立一致且可維護的錯誤處理流程,提升整體系統的可靠度與開發效率。祝你寫程式愉快,錯誤不再是阻礙,而是提升品質的契機!