JavaScript 控制流程:throw 與錯誤處理
簡介
在任何程式語言中,**錯誤(Error)**都是不可避免的。無論是使用者輸入不符合預期、外部資源(如檔案、網路)無法取得,或是程式本身的邏輯缺陷,都可能在執行時拋出例外。若不妥善處理,錯誤會直接導致程式中斷、使用者體驗變差,甚至在伺服器端留下安全隱憂。
JavaScript 提供了 throw 關鍵字與 try…catch…finally 陳述式,讓開發者能夠主動拋出自訂例外,並在適當的層級捕捉、處理或記錄這些錯誤。掌握這套機制不僅能讓程式碼更具韌性,也能提升除錯效率、降低維護成本。
本篇文章將從 throw 的語法與用途、例外類型、try…catch 的運作原理,到 實務上常見的陷阱與最佳實踐,一步步帶你建立完整的錯誤處理觀念。
核心概念
1. 為什麼要使用 throw
- 主動報錯:當程式偵測到不合規的狀況(例如參數為
null),可以立即拋出例外,避免錯誤往下傳播造成更難追蹤的問題。 - 分離錯誤與正常流程:
throw讓錯誤處理與主要業務邏輯分開,使程式碼更易讀。 - 自訂錯誤類型:開發者可以自行建立錯誤物件,提供更具語意的錯誤資訊。
基本語法
throw expression; // expression 可以是任何值,常見是 Error 物件
注意:一旦執行
throw,當前函式的執行立即停止,控制權交給最近的catch區塊(若有),否則會沿呼叫堆疊往上冒泡,最終可能導致程式終止。
2. 內建錯誤類型
JavaScript 內建了幾種常用的錯誤類別,皆繼承自 Error:
| 錯誤類型 | 何時拋出 | 範例 |
|---|---|---|
SyntaxError |
語法錯誤(在解析階段) | eval('var a ='); |
ReferenceError |
變數未宣告或找不到 | console.log(notDefined); |
TypeError |
操作不符合型別(例如對 null 呼叫方法) |
null.length; |
RangeError |
數值超出允許範圍(例如陣列長度負值) | new Array(-1); |
EvalError |
eval 使用不當(已較少出現) |
|
URIError |
URI 編碼/解碼失敗 | decodeURIComponent('%'); |
小技巧:在自訂錯誤時,繼承自
Error能夠保留堆疊資訊(stack trace),方便除錯。
3. 自訂錯誤類別
class ValidationError extends Error {
constructor(message, field) {
super(message); // 設定錯誤訊息
this.name = 'ValidationError'; // 自訂錯誤名稱
this.field = field; // 可自行加入額外屬性
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ValidationError);
}
}
}
// 使用範例
function validateAge(age) {
if (typeof age !== 'number') {
throw new ValidationError('年齡必須是數字', 'age');
}
if (age < 0 || age > 120) {
throw new ValidationError('年齡超出合理範圍', 'age');
}
}
此範例展示如何將錯誤資訊(例如是哪個欄位驗證失敗)直接包在錯誤物件中,讓上層的 catch 能更精確地處理。
4. try…catch…finally 的運作流程
try {
// 可能會拋出例外的程式碼
} catch (err) {
// 捕捉例外,err 為拋出的值
} finally {
// 不管有沒有例外,都會執行的清理工作
}
try區塊:放置可能發生錯誤的程式碼。catch區塊:只有在try內真的拋出例外時才會執行。可以針對不同錯誤類型分別處理。finally區塊:常用於釋放資源(關閉檔案、斷開連線),即使catch中又拋出新例外,它仍會執行。
例外冒泡(Exception Propagation)
function level1() {
level2(); // 若 level2 拋出例外,會往上傳遞
}
function level2() {
throw new Error('從 level2 拋出的錯誤');
}
try {
level1();
} catch (e) {
console.error('捕捉到錯誤:', e.message);
}
此機制讓開發者可以在較高層級統一處理錯誤,而不必在每一層都寫 try…catch。
5. 程式碼範例
以下提供 5 個實用範例,從基礎到進階,說明 throw 與錯誤處理的常見寫法。
範例 1:簡單的參數檢查
function divide(a, b) {
if (b === 0) {
// 主動拋出錯誤,避免除以零的算術異常
throw new Error('除數不能為零');
}
return a / b;
}
try {
console.log(divide(10, 2)); // 5
console.log(divide(5, 0)); // 觸發例外
} catch (e) {
console.warn('運算失敗:', e.message);
}
要點:使用
throw讓錯誤訊息更具體,且在catch中只需要一次統一處理。
範例 2:自訂驗證錯誤
class InvalidUserError extends Error {
constructor(message) {
super(message);
this.name = 'InvalidUserError';
}
}
function getUser(id) {
const fakeDB = { 1: 'Alice', 2: 'Bob' };
if (!Number.isInteger(id)) {
throw new InvalidUserError('使用者 ID 必須是整數');
}
const name = fakeDB[id];
if (!name) {
throw new InvalidUserError(`找不到 ID 為 ${id} 的使用者`);
}
return { id, name };
}
try {
const user = getUser('a'); // 會拋出 InvalidUserError
console.log(user);
} catch (err) {
if (err instanceof InvalidUserError) {
console.error('使用者驗證失敗:', err.message);
} else {
console.error('未知錯誤:', err);
}
}
說明:透過
instanceof判斷錯誤類型,可在同一個catch中做差異化處理。
範例 3:非同步函式的錯誤傳遞
async function fetchJson(url) {
const response = await fetch(url);
if (!response.ok) {
// 仍然使用 throw,只是現在它會成為 Promise 被 reject 的原因
throw new Error(`網路錯誤:${response.status}`);
}
return response.json(); // 仍可能拋出語法錯誤
}
// 使用 try…catch 搭配 await
(async () => {
try {
const data = await fetchJson('https://api.example.com/data');
console.log('取得資料:', data);
} catch (e) {
console.error('取得資料失敗:', e.message);
}
})();
重點:在
async函式內拋出的錯誤會自動轉成 Promise 被 reject,因此外層的await必須配合try…catch。
範例 4:資源釋放 – finally 的力量
function readFileSync(path) {
const fs = require('fs');
let fd;
try {
fd = fs.openSync(path, 'r');
const buffer = Buffer.alloc(1024);
const bytesRead = fs.readSync(fd, buffer, 0, 1024, null);
return buffer.slice(0, bytesRead).toString('utf8');
} catch (e) {
console.error('讀檔失敗:', e.message);
throw e; // 重新拋出,讓上層知道失敗
} finally {
if (fd !== undefined) {
fs.closeSync(fd); // 無論成功或失敗,都必須關閉檔案描述符
}
}
}
實務觀點:
finally確保 資源不會遺漏釋放,是寫 I/O、資料庫連線等程式時的必備技巧。
範例 5:全域錯誤捕捉(Node.js 與瀏覽器)
// Node.js 中的全域未捕捉例外
process.on('uncaughtException', (err) => {
console.error('未捕捉的例外:', err);
// 可選擇寫入日誌、上報監控,最後安全退出
process.exit(1);
});
// 瀏覽器中的全域錯誤處理
window.addEventListener('error', (event) => {
console.error('全域錯誤:', event.error);
// 顯示友善的 UI、回報至錯誤追蹤服務
});
提醒:全域捕捉僅作為最後防線,仍建議在業務邏輯層面盡可能使用局部的
try…catch。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 最佳做法 |
|---|---|---|
拋出字串或非 Error 物件 |
throw "Error" 雖可運作,但失去堆疊資訊,除錯困難。 |
永遠拋出 Error 或其子類別,必要時自訂錯誤類別。 |
在 catch 中沉默錯誤 |
catch (e) {} 直接忽略錯誤會讓問題隱藏,難以追蹤。 |
至少記錄錯誤(console.error、日誌服務),或重新拋出。 |
過度使用 try…catch |
每一行程式都包 try 會降低可讀性,且效能略有影響。 |
只在可能失敗的操作或邊界條件(I/O、網路、解析)使用 try…catch。 |
忘記 finally 釋放資源 |
例外發生後資源(檔案、DB 連線)未關閉,會造成資源泄漏。 | 使用 finally 或 using / with 類似模式(如 fs.promises 的 finally)。 |
在非同步回呼中使用 throw |
throw 只會在同步堆疊中被捕捉,非同步回呼會導致程式崩潰。 |
改用 reject(Promise)或回呼的錯誤參數,或在 async/await 中 throw。 |
| 忽略錯誤類型的區分 | 把所有錯誤都當成同一類處理,會失去針對性。 | 使用 instanceof 或 err.name 進行細分處理。 |
其他實用技巧
- 建立錯誤工廠(Error Factory):集中管理錯誤訊息與代碼,方便國際化與維護。
- 錯誤堆疊過濾:在生產環境只保留必要的堆疊資訊,以免洩漏內部實作。
- 結合 TypeScript:利用型別系統提前捕捉可能的
null/undefined,減少 runtime 錯誤。 - 使用
assert:在開發階段使用console.assert或 Node.js 的assert模組,快速驗證前置條件。
實際應用場景
1. 表單驗證與 API 回傳錯誤
在前端開發時,使用者提交表單前先在客戶端驗證;若驗證失敗,拋出自訂 ValidationError,在全局 catch 中統一顯示錯誤訊息;若 API 回傳錯誤(如 400、500),在 fetch 的回應處理階段 throw 具體的錯誤物件,讓 UI 能根據錯誤類型顯示不同的提示。
2. Node.js 服務的交易 (Transaction)
在資料庫操作(如 MySQL、MongoDB)時,使用 try…catch…finally 包住交易流程:
try:開始交易、執行多個寫入操作。catch:若任一步驟失敗,rollback交易,並拋出自訂的TransactionError。finally:釋放連線池資源。
這樣即使程式在中途拋出例外,也能保證資料一致性。
3. 微服務間的錯誤傳遞
在微服務架構中,服務 A 呼叫服務 B,若 B 回傳錯誤代碼,A 可以 將錯誤資訊重新包裝成自訂錯誤(例如 RemoteServiceError),再拋出。上層的 API Gateway 再根據錯誤類型回傳適當的 HTTP 狀態碼與錯誤訊息給前端,保持錯誤資訊的完整性與可追蹤性。
4. 定時任務(Cron Job)與容錯
定時任務常在背景執行,若某一步驟失敗不應讓整個任務停擺。可在每個子任務內 捕捉錯誤並記錄,同時讓主流程 繼續執行;若錯誤屬於致命(例如無法取得必要的設定檔),則拋出讓外層的排程系統感知失敗,進行重試或通知。
總結
throw是 主動拋出例外 的關鍵字,配合Error及其子類別可提供完整的錯誤資訊與堆疊追蹤。try…catch…finally讓我們能在 同步與非同步 程式碼中安全捕捉、處理與清理。- 自訂錯誤類別 能讓錯誤更具語意,方便在上層做差異化處理。
- 常見陷阱包括拋出非
Error物件、在catch中沉默錯誤、忘記釋放資源等;遵循 最佳實踐(只在必要處使用try…catch、記錄錯誤、使用finally)能讓程式更可靠。 - 在實務開發中,錯誤處理不僅是防止程式崩潰,更是提升使用者體驗、確保資料一致性、與外部系統協作的基礎。
掌握了 throw 與錯誤處理的技巧,你就能寫出更健全、可維護的 JavaScript 程式,無論是前端 UI、Node.js 後端,或是跨服務的分散式系統,都能從容面對各種不可預期的情況。祝你在開發旅程中,錯誤不再是阻礙,而是提升品質的好幫手!