本文 AI 產出,尚未審核

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 連線)未關閉,會造成資源泄漏。 使用 finallyusing / with 類似模式(如 fs.promisesfinally)。
在非同步回呼中使用 throw throw 只會在同步堆疊中被捕捉,非同步回呼會導致程式崩潰。 改用 reject(Promise)或回呼的錯誤參數,或在 async/awaitthrow
忽略錯誤類型的區分 把所有錯誤都當成同一類處理,會失去針對性。 使用 instanceoferr.name 進行細分處理。

其他實用技巧

  1. 建立錯誤工廠(Error Factory):集中管理錯誤訊息與代碼,方便國際化與維護。
  2. 錯誤堆疊過濾:在生產環境只保留必要的堆疊資訊,以免洩漏內部實作。
  3. 結合 TypeScript:利用型別系統提前捕捉可能的 null/undefined,減少 runtime 錯誤。
  4. 使用 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 後端,或是跨服務的分散式系統,都能從容面對各種不可預期的情況。祝你在開發旅程中,錯誤不再是阻礙,而是提升品質的好幫手!