本文 AI 產出,尚未審核

JavaScript 錯誤與除錯:try / catch / finally 完全指南


簡介

在日常開發中,程式的執行不可能永遠順利,例外 (Exception) 隨時可能被拋出。若未妥善處理,錯誤會直接中斷程式,造成使用者不好的體驗,甚至產生安全漏洞。JavaScript 為了讓開發者能在錯誤發生時仍能控制流程,提供了 try / catch / finally 三大關鍵字,形成完整的錯誤捕捉與清理機制。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你了解如何在 Node.js瀏覽器 環境中,安全且有效率地使用 try / catch / finally,提升除錯效率與程式的韌性。


核心概念

1. try 區塊:執行可能拋出例外的程式碼

try 會先執行其內部的程式碼,若在執行過程中產生 例外,JavaScript 立即中斷 try,跳到相對應的 catch 區塊。若沒有例外,catch 會被略過,直接進入 finally(若有的話)。

try {
  // 可能會產生錯誤的程式
  const data = JSON.parse('{"name":"Alice"}');
  console.log(data);
} catch (err) {
  // 這裡只會在 try 內拋出錯誤時被執行
  console.error('JSON 解析失敗:', err);
}

2. catch 區塊:捕捉並處理錯誤

catch 接收一個參數(慣稱 err),代表被拋出的 Error 物件。此區塊的主要任務是紀錄、回報或補救錯誤,而不是讓錯誤直接傳到全域。

try {
  // 故意除以零,會產生 NaN,但不會拋出例外
  const result = 10 / 0;
  if (!isFinite(result)) throw new Error('除以零');
} catch (err) {
  // 這裡可以自行決定要怎麼處理
  console.warn('運算錯誤:', err.message);
}

小技巧:在 catch 內可以使用 instanceof 判斷錯誤類型,針對不同錯誤做不同的處理。

3. finally 區塊:不管成功或失敗,都一定會執行

finally 常用於資源釋放狀態重置關閉連線等必須執行的清理工作。即使 try 內有 returnbreakcontinue,或 catch 再拋出錯誤,finally 仍會在最後執行。

let fileHandle;
try {
  fileHandle = openFile('data.txt'); // 假想的同步檔案開啟
  const content = fileHandle.read();
  console.log(content);
} catch (err) {
  console.error('讀檔失敗:', err);
} finally {
  // 確保檔案一定被關閉
  if (fileHandle) fileHandle.close();
  console.log('資源清理完畢');
}

4. 嵌套與多層 try / catch

在複雜流程中,常需要分層捕捉錯誤。例如:外層負責針對整體流程的回報,內層只處理特定步驟的錯誤。

function fetchData(url) {
  try {
    const response = fetch(url); // 可能拋出 TypeError
    try {
      if (!response.ok) throw new Error('HTTP 錯誤 ' + response.status);
      return response.json(); // 仍可能拋出 SyntaxError
    } catch (innerErr) {
      console.warn('回應處理失敗:', innerErr);
      throw innerErr; // 重新拋出讓外層捕捉
    }
  } catch (outerErr) {
    console.error('取得資料失敗:', outerErr);
    // 此處可回傳預設值或重新拋出
    return null;
  }
}

5. 非同步環境中的 try / catch

Promiseasync/await 中,try / catch 仍然適用,只是要把可能拋出的非同步錯誤包在 await 表達式裡。

async function readJson(filePath) {
  try {
    const text = await fs.promises.readFile(filePath, 'utf8');
    return JSON.parse(text);
  } catch (err) {
    // 讀檔或 JSON 解析失敗都會被捕捉
    console.error('讀取或解析失敗:', err);
    return null; // 回傳安全的預設值
  } finally {
    console.log('readJson 結束');
  }
}

程式碼範例

以下提供 5 個實務範例,每個範例皆附上說明與最佳做法。

範例 1:表單驗證與錯誤回報

function validateForm(data) {
  try {
    if (!data.username) throw new Error('使用者名稱不可空白');
    if (data.age < 0) throw new RangeError('年齡不能為負數');
    // 其他驗證...
    return true;
  } catch (err) {
    // 只回傳錯誤訊息給 UI
    displayError(err.message);
    return false;
  } finally {
    // 無論成功與否,都清除暫存的驗證狀態
    resetValidationState();
  }
}

範例 2:API 呼叫的容錯機制

async function getUser(id) {
  const url = `https://api.example.com/users/${id}`;
  try {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`伺服器回傳 ${res.status}`);
    return await res.json();
  } catch (err) {
    // 網路錯誤或伺服器錯誤都會走到這裡
    console.warn('取得使用者失敗,使用快取資料', err);
    return getUserFromCache(id); // 從本地快取回傳
  } finally {
    // 記錄本次請求的時間戳記,方便日後分析
    logRequest(url);
  }
}

範例 3:資料庫交易 (Transaction) 的保證執行

async function transferFunds(db, fromAcc, toAcc, amount) {
  const trx = await db.beginTransaction();
  try {
    await trx.debit(fromAcc, amount);
    await trx.credit(toAcc, amount);
    await trx.commit(); // 若成功,提交交易
    console.log('轉帳成功');
  } catch (err) {
    await trx.rollback(); // 發生錯誤,回滾
    console.error('轉帳失敗,已回滾', err);
    throw err; // 讓呼叫端知道失敗
  } finally {
    await trx.release(); // 釋放連線資源
    console.log('交易結束');
  }
}

範例 4:自訂錯誤類別與型別判斷

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field; // 失敗的欄位名稱
  }
}

function processInput(input) {
  try {
    if (typeof input !== 'string') {
      throw new ValidationError('必須為字串', 'input');
    }
    // 其他處理...
  } catch (err) {
    if (err instanceof ValidationError) {
      console.error(`欄位 ${err.field} 驗證失敗:${err.message}`);
    } else {
      console.error('未知錯誤:', err);
    }
  }
}

範例 5:使用 finally 進行效能計時

function measure(fn) {
  const start = performance.now();
  try {
    return fn();
  } finally {
    const end = performance.now();
    console.log(`執行時間:${(end - start).toFixed(2)} ms`);
  }
}

// 範例使用
measure(() => {
  // 需要測量的程式碼
  for (let i = 0; i < 1e6; i++) {}
});

常見陷阱與最佳實踐

陷阱 為何會發生 建議的做法
忘記加 finally 資源(檔案、網路連線)可能遺漏釋放,導致記憶體泄漏。 總是在需要清理的地方加入 finally,即使目前看不到錯誤。
catch 中再拋出錯誤卻未被外層捕捉 會讓錯誤「跑到全域」造成程式崩潰。 若要重新拋出,確保外層有相對應的 try / catch,或使用 process.on('unhandledRejection') 捕捉未處理的 Promise。
catch 內使用 throw err; 失去原始堆疊資訊 重新拋出同一個錯誤會保留堆疊,但若自行 new Error(err) 會失去資訊。 直接 throw err;,或使用 Error.cause(Node 16+)保留原始錯誤。
在同步 try 包住非同步回呼 回呼在 try 執行完畢後才會觸發,錯誤不會被捕捉。 使用 Promiseasync/await,把非同步程式碼搬到 await 前的 try 中。
過度捕捉所有錯誤 會掩蓋程式的真正問題,導致除錯困難。 只捕捉 可預期 的錯誤,對未知錯誤可以記錄後再拋出或讓全域處理。

最佳實踐總結

  1. 只捕捉必要的錯誤:使用 instanceof 或自訂屬性篩選。
  2. 保留錯誤資訊console.error(err) 或使用日誌框架(如 Winston)記錄堆疊。
  3. 確保資源釋放finally 中寫入 close / release 等清理程式。
  4. async/await 簡化非同步錯誤處理:避免「回呼地獄」與遺漏捕捉。
  5. 統一錯誤格式:自訂錯誤類別,讓前端或 API 客戶端能一致解析。

實際應用場景

場景 為什麼需要 try / catch / finally 示例
前端表單送出 使用者可能填入非法資料,需即時提示並避免送出錯誤請求。 validateForm 範例
呼叫第三方 API 網路波動或服務端錯誤會拋出例外,必須回退到快取或顯示錯誤訊息。 getUser 範例
資料庫交易 多個寫入必須原子性,任一失敗需回滾,且連線必須釋放。 transferFunds 範例
背景排程 (Cron) 定時任務若失敗不應阻塞下一輪,需記錄失敗並釋放資源。 measure + finally
Node.js 服務端中介層 中介層要捕捉下層的錯誤,統一回傳 JSON 錯誤格式,且保證請求結束後釋放資料流。 Express error‑handling middleware(app.use((err, req, res, next) => {...})

總結

try / catch / finallyJavaScript 中最基礎、同時也是最強大的錯誤處理工具。透過正確的 例外捕捉資訊保留資源清理,我們可以:

  • 防止程式因未處理的例外而直接崩潰
  • 為使用者提供友善的錯誤回饋
  • 確保檔案、資料庫、網路等資源在任何情況下都能被妥善釋放
  • 在非同步程式碼中保持一致的錯誤流程

在日常開發中,養成 只捕捉必要錯誤、使用 finally 釋放資源、以自訂錯誤類別統一錯誤格式 的習慣,能大幅提升程式的可維護性與韌性。希望本篇文章能讓你在實務專案中,從容面對各種例外情況,寫出更安全、更可靠的 JavaScript 程式碼。祝開發順利!