本文 AI 產出,尚未審核

JavaScript 控制流程 – try / catch / finally 完全指南


簡介

在日常開發中,我們常常需要處理 不可預期的錯誤:網路斷線、JSON 解析失敗、檔案讀寫錯誤… 只要程式拋出例外而沒有適當的處理,整個應用程式就會直接中斷,使用者體驗會大幅下降。
JavaScript 提供的 try / catch / finally 機制,讓我們能夠 捕捉例外、做適當的回復或清理資源,從而讓程式在錯誤發生時仍能保持穩定運作。

本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整介紹 try / catch / finally 的使用方式,適合 初學者 了解基礎,也能讓 中階開發者 在實務專案中更安心地運用例外處理。


核心概念

1. try 區塊 – 可能拋出例外的程式碼

try 內部放置可能發生錯誤的程式碼。只要在執行過程中發生例外,控制權會立即跳到最近的 catch(如果有的話)。

try {
  // 可能拋出例外的程式碼
  const data = JSON.parse(responseText);
  console.log('解析成功:', data);
} 
  • 注意:try 必須搭配至少一個 catch 或 finally,單獨使用會產生語法錯誤。

2. catch 區塊 – 捕捉與處理例外

catch 會接收一個參數(常見名稱為 error),代表拋出的例外物件。此區塊內可以記錄錯誤、回傳預設值、或顯示友善訊息。

try {
  const data = JSON.parse(responseText);
  console.log('解析成功:', data);
} catch (error) {
  // 捕捉錯誤,避免程式直接崩潰
  console.error('JSON 解析失敗:', error.message);
  // 可自行決定回傳什麼
  const fallback = { success: false };
  console.log('使用預設值:', fallback);
}
  • error 物件通常具備 name、message、stack 等屬性,可用來判斷錯誤類型。

3. finally 區塊 – 無論成功或失敗都會執行

finally 會在 try 完成(無論是否拋出例外)以及 catch 執行完畢後一定會被呼叫。常用於釋放資源、關閉檔案、停止載入動畫等清理工作。

let loadingSpinner = document.getElementById('spinner');
loadingSpinner.style.display = 'block';

try {
  const data = JSON.parse(responseText);
  console.log('解析成功:', data);
} catch (error) {
  console.error('JSON 解析失敗:', error.message);
} finally {
  // 不管成功或失敗,都要隱藏 loading
  loadingSpinner.style.display = 'none';
}
  • 即使在 catch 中使用 return、throw,finally 仍會先執行。

4. 多層 try...catch...finally

在大型程式中,例外可能在不同層級傳遞。外層的 try 可以捕捉內層未處理的錯誤,形成階層式的錯誤處理。

function fetchData(url) {
  try {
    const response = fetch(url);
    // 假設這裡拋出錯誤
    return response.json();
  } catch (innerError) {
    console.warn('內層 fetch 錯誤,重新拋出');
    throw innerError; // 重新拋出給外層處理
  }
}

try {
  const data = await fetchData('https://api.example.com/data');
  console.log(data);
} catch (outerError) {
  console.error('外層捕捉到錯誤:', outerError);
}

程式碼範例

以下提供 5 個實用範例,展示不同情境下的 try / catch / finally 用法。

範例 1:安全的 JSON 解析

function safeParse(jsonString) {
  try {
    return JSON.parse(jsonString);
  } catch (e) {
    console.error('JSON 解析失敗:', e.message);
    // 回傳空物件或其他預設值
    return {};
  }
}

// 使用
const obj = safeParse('{"name":"Alice"}');   // 正常
const bad = safeParse('invalid json');      // 失敗,返回 {}

重點:將可能失敗的操作封裝成函式,讓呼叫端不必每次都寫 try/catch。


範例 2:非同步函式的錯誤處理(async/await)

async function loadUser(id) {
  try {
    const resp = await fetch(`/api/users/${id}`);
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    const user = await resp.json();
    return user;
  } catch (err) {
    console.error('載入使用者失敗:', err);
    // 可回傳預設使用者或 null
    return null;
  } finally {
    console.log('loadUser 執行結束 (不論成功或失敗)');
  }
}

技巧:在 async 函式裡使用 try/catch,可以捕捉 await 產生的 rejected Promise。


範例 3:資源釋放 – 讀檔後必須關閉

const fs = require('fs');

function readFileSyncSafe(path) {
  let fd;
  try {
    fd = fs.openSync(path, 'r');
    const buffer = Buffer.alloc(1024);
    fs.readSync(fd, buffer, 0, buffer.length, 0);
    return buffer.toString();
  } catch (e) {
    console.error('讀檔失敗:', e.message);
    return null;
  } finally {
    if (fd !== undefined) {
      fs.closeSync(fd);   // 確保檔案描述符一定被關閉
      console.log('檔案已關閉');
    }
  }
}

關鍵:finally 是釋放外部資源(檔案、網路連線、資料庫連線)的最佳場所。


範例 4:自訂例外類別與條件捕捉

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError';
  }
}

function validateUser(user) {
  if (!user.email.includes('@')) {
    throw new ValidationError('Email 格式不正確');
  }
  // 其他驗證...
}

try {
  validateUser({ email: 'invalid-email' });
} catch (e) {
  if (e instanceof ValidationError) {
    console.warn('使用者輸入錯誤:', e.message);
  } else {
    console.error('未知錯誤:', e);
  }
}

說明:透過自訂錯誤類別,可在 catch 中精準分類不同類型的例外。


範例 5:finally 中的 return 與例外傳遞

function demoFinally() {
  try {
    console.log('try 區塊執行');
    // 這裡不拋出錯誤
    return 'from try';
  } catch (e) {
    return 'from catch';
  } finally {
    console.log('finally 執行');
    // 若在 finally 中回傳,會覆寫前面的 return
    // return 'from finally'; // 取消註解會改變最終結果
  }
}

console.log(demoFinally()); // 輸出: from try

提醒:finally 若自行回傳或拋出例外,會覆寫 try/catch 的結果,務必小心使用。


常見陷阱與最佳實踐

常見陷阱 為什麼會發生 解決方案 / 最佳實踐
在 catch 中忘記拋出錯誤 開發者只記錄錯誤,未將錯誤向上層傳遞,導致上層無法感知失敗 若無法在此層完整處理,throw error 重新拋出
finally 裡的 return 覆寫結果 finally 執行後仍可回傳值,會覆寫 try/catch 的返回 避免在 finally 中使用 return,僅做清理工作
捕捉過於寬泛的例外 catch (e) 會捕捉所有錯誤,可能把程式錯誤當成可預期錯誤處理 使用 條件判斷(if (e instanceof TypeError))或 自訂錯誤類別
同步錯誤被非同步 Promise 隱蔽 在 async 函式裡,忘記 await 會讓錯誤變成未處理的 rejected Promise 確保所有返回 Promise 的呼叫都使用 await 或 .catch()
忘記關閉資源 資源(檔案、DB 連線)在例外發生時未釋放,造成資源泄漏 把 關閉/釋放 動作放在 finally 中,保證一定會執行

最佳實踐小結

  1. 只捕捉可預期的錯誤:例如網路失敗、使用者輸入錯誤。程式碼錯誤(ReferenceError)應該讓它冒泡,讓開發者在測試階段發現。
  2. 保持 try 區塊盡量小:只包住可能拋出例外的程式碼,避免把大量業務邏輯塞進去,讓除錯更容易。
  3. 使用自訂錯誤類別:提升錯誤辨識度,讓 catch 能針對不同情況做不同處理。
  4. 在 finally 中只做清理:不要放置會改變程式流程的邏輯(如 return、throw),以免產生難以預期的行為。
  5. 在非同步環境:async/await 搭配 try/catch,或在 Promise 鏈最後加 .catch(),確保所有 rejected 都被捕捉。

實際應用場景

場景 為何需要 try / catch / finally 典型寫法
API 呼叫失敗 網路斷線、伺服器回傳 5xx,若不處理會讓 UI 卡住 await fetch(...).then(...).catch(err => showError())
使用者表單驗證 輸入不符合規範時拋出自訂 ValidationError,提示使用者修正 try { validate(form); submit(); } catch (e) { if (e instanceof ValidationError) showMsg(e.message); }
檔案上傳與暫存 上傳過程中可能因檔案過大或權限問題失敗,需要釋放暫存空間 try { upload(); } catch (e) { cleanupTemp(); throw e; } finally { hideProgressBar(); }
資料庫交易 (Transaction) 多個 DB 操作必須全部成功,否則回滾 (rollback) try { begin(); insert(); update(); commit(); } catch (e) { rollback(); } finally { closeConnection(); }
第三方套件整合 第三方函式庫可能拋出例外,若不捕捉會導致整個應用崩潰 try { thirdParty.doSomething(); } catch (e) { log(e); fallback(); }

總結

try / catch / finally 是 JavaScript 例外處理的核心工具,透過它我們可以:

  • 捕捉 可能發生的錯誤,避免程式直接崩潰。
  • 回復 或 提供預設值,讓使用者體驗更友好。
  • 清理資源(關閉檔案、停止動畫、釋放記憶體),確保系統長時間運作不會泄漏。

在實務開發中,遵守「只捕捉可預期錯誤、保持 try 區塊精簡、finally 只做清理」的原則,能讓程式碼更易讀、易維護,也更不容易因未處理的例外而產生不可預期的錯誤。

掌握好這套機制,你就能在 前端、Node.js、甚至跨平台的 JavaScript 應用 中,寫出更健壯、更可靠的程式。祝你在開發旅程中,錯誤不再是阻礙,而是提升品質的好幫手!