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 內有 return、break、continue,或 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
在 Promise 或 async/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 執行完畢後才會觸發,錯誤不會被捕捉。 |
使用 Promise 或 async/await,把非同步程式碼搬到 await 前的 try 中。 |
| 過度捕捉所有錯誤 | 會掩蓋程式的真正問題,導致除錯困難。 | 只捕捉 可預期 的錯誤,對未知錯誤可以記錄後再拋出或讓全域處理。 |
最佳實踐總結
- 只捕捉必要的錯誤:使用
instanceof或自訂屬性篩選。 - 保留錯誤資訊:
console.error(err)或使用日誌框架(如 Winston)記錄堆疊。 - 確保資源釋放:
finally中寫入close / release等清理程式。 - 用
async/await簡化非同步錯誤處理:避免「回呼地獄」與遺漏捕捉。 - 統一錯誤格式:自訂錯誤類別,讓前端或 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 / finally 是 JavaScript 中最基礎、同時也是最強大的錯誤處理工具。透過正確的 例外捕捉、資訊保留 與 資源清理,我們可以:
- 防止程式因未處理的例外而直接崩潰
- 為使用者提供友善的錯誤回饋
- 確保檔案、資料庫、網路等資源在任何情況下都能被妥善釋放
- 在非同步程式碼中保持一致的錯誤流程
在日常開發中,養成 只捕捉必要錯誤、使用 finally 釋放資源、以自訂錯誤類別統一錯誤格式 的習慣,能大幅提升程式的可維護性與韌性。希望本篇文章能讓你在實務專案中,從容面對各種例外情況,寫出更安全、更可靠的 JavaScript 程式碼。祝開發順利!