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 中,保證一定會執行 |
最佳實踐小結
- 只捕捉可預期的錯誤:例如網路失敗、使用者輸入錯誤。程式碼錯誤(ReferenceError)應該讓它冒泡,讓開發者在測試階段發現。
- 保持
try區塊盡量小:只包住可能拋出例外的程式碼,避免把大量業務邏輯塞進去,讓除錯更容易。 - 使用自訂錯誤類別:提升錯誤辨識度,讓
catch能針對不同情況做不同處理。 - 在
finally中只做清理:不要放置會改變程式流程的邏輯(如return、throw),以免產生難以預期的行為。 - 在非同步環境:
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 應用 中,寫出更健壯、更可靠的程式。祝你在開發旅程中,錯誤不再是阻礙,而是提升品質的好幫手!