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…finally 與 throw 的配合
try {
// 可能會拋出錯誤的程式碼
} catch (err) {
// 錯誤處理
} finally {
// 無論是否有錯誤,都會執行的清理工作
}
try:放置可能拋出例外的程式碼。catch:捕捉throw或執行階段產生的錯誤,err參數即為拋出的值。finally:常用於釋放資源(如關閉檔案、清除計時器),即使catch內部再次throw,finally仍會被執行。
程式碼範例
以下示範 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 或其子類別(可自行擴充)。 |
| 在非同步環境忘記捕捉 | throw 在 Promise、async/await 中若未 catch,會導致未處理的 rejected。 |
使用 try…catch 包住 await,或在 Promise 鏈最後加 .catch()。 |
過度使用 throw |
每一次 throw 都會中斷執行,過度拋出會降低效能並使程式流程難以追蹤。 |
僅在真正的異常狀況(非預期、無法自行修復)時使用 throw,否則回傳錯誤資訊或使用 Result 物件。 |
在 catch 裏直接 swallow 錯誤 |
隱藏問題,導致後續 bug 難以偵測。 | 適當記錄或重新拋出(throw err;),除非確定錯誤已被完整處理。 |
忘記在 finally 釋放資源 |
可能造成記憶體洩漏、檔案鎖定等問題。 | 在 finally 中必定執行清理程式碼。 |
最佳實踐清單
- 統一錯誤類別:在專案根目錄建立
errors.js,集中管理自訂錯誤。 - 錯誤訊息要具可讀性:包含「發生什麼」與「在哪裡」的資訊,避免只寫「錯誤」。
- 保留原始錯誤:若要包裝錯誤,使用
cause(ES2022)或自行保存originalError,方便追蹤。throw new Error("高階錯誤訊息", { cause: originalError }); - 在非同步函式中使用
async/await+try…catch:比起.then().catch()更易閱讀。 - 記錄錯誤:在
catch中使用日誌框架(如winston、pino)寫入檔案或遠端服務,避免僅在 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 使用 sequelize 或 typeorm 時,若在交易過程中發生任何錯誤,都會 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、資料庫交易 等各種情境中,建立一致且可維護的錯誤處理流程,提升整體系統的可靠度與開發效率。祝你寫程式愉快,錯誤不再是阻礙,而是提升品質的契機!