本文 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 必須搭配至少一個 catchfinally,單獨使用會產生語法錯誤。

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 物件通常具備 namemessagestack 等屬性,可用來判斷錯誤類型

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 中使用 returnthrowfinally 仍會先執行。

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 中只做清理:不要放置會改變程式流程的邏輯(如 returnthrow),以免產生難以預期的行為。
  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 / finallyJavaScript 例外處理的核心工具,透過它我們可以:

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

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

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