iT邦幫忙

2025 iThome 鐵人賽

DAY 24
2
Software Development

消除你程式碼的臭味系列 第 24

Day 24- 錯誤處理:別讓程式崩潰

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250903/20124462P1N8QjGguI.png

消除你程式碼的臭味 Day 24- 錯誤處理:別讓程式崩潰

錯誤不是你的敵人,忽略它才是。

程式碼在實際情況裡運行,就一定會出錯。
網路會斷、硬碟會滿、使用者會輸入亂七八糟的東西。

錯誤處理的目的,就是讓系統在混亂中依然能健壯可預測地運行。

一個函式的合約只有兩個結果:成功完成它的工作,或者清楚地報告它為什麼失敗。
沒有第三個選項。

經典案例:把錯誤吞掉

// 🔴 掩蓋問題的臭味道
try {
  doSomethingCritical();
} catch (e) {
  // 這個空白,是留給未來維護者的定時炸彈
}

這就像車子的引擎警示燈亮了,卻用黑色膠帶把它貼起來。
問題還在,只是從「已知」變成「未知」,隨時可能引爆。

主要原則:就地處理,或者向上拋出 (Handle or Throw)

捕捉到一個錯誤,只有兩種選擇。

1. 有能力處理,就地解決

所謂「處理」,是指你能從錯誤中完全恢復,讓函式繼續成功完成任務。

  • 重試:網路請求失敗?也許再試一次就好。

  • 使用備案:讀取設定檔失敗?改用一個安全的預設值。

// 🟡 這才叫「處理」:我們從錯誤中恢復了。
function getConfig() {
  try {
    return JSON.parse(fs.readFileSync('/path/to/config.json'));
  } catch (e) {
    // 讀取失敗,沒關係,我們有備案,函式依然能成功。
    log.warn('Config file failed to load, using default values.');
    return { timeout: 5000, retries: 3 }; // 回傳預設值
  }
}

如果無法完全恢復,那就不叫「處理」,那叫「掩蓋錯誤」。

2. 無法處理,往上拋出。

當你的函式沒有足夠的資訊來處理錯誤時,你的責任就是立即將問題報告給更高層的呼叫者。

一個好的拋出,不只是 throw e;,而是會為錯誤增添價值

  • 記錄它 (Log):留下詳細的堆疊追蹤和相關變數,這是未來除錯的唯一線索。

  • 包裝它 (Wrap):將低階、模糊的錯誤包裝成有業務意義的高階錯誤( PaymentGatewayError)。
    這能建立清晰的抽象邊界,避免底層實作細節洩漏。

// 🟢 好味道:我處理不了,但我會清楚地報告一個有意義的問題

// 定義一個自訂的、有業務意義的錯誤類型
class PaymentGatewayError extends Error {
  constructor(message, cause) {
    super(message);
    this.name = 'PaymentGatewayError';
    this.cause = cause; // 保留原始錯誤,方便追蹤
  }
}

async function chargeCustomer(customerId, amount) {
  try {
    // 在 async 函式中,若 Promise 沒有後續處理,可直接回傳
    return paymentGateway.charge(amount);
  } catch (e) {
    // 1. 記錄詳細資訊,給維護者看
    log.error(`Payment gateway failed for customer ${customerId}`, { error: e });
    // 2. 拋出一個新的、有意義的錯誤,給呼叫者處理
    throw new PaymentGatewayError('Failed to charge customer.', e);
  }
}

https://ithelp.ithome.com.tw/upload/images/20250926/20124462Nfjeos2dl1.png

別忘了清理:finally 的角色

無論 try 區塊是成功還是失敗,有些事必定要執行,常用在「資源的釋放」,這就是 finally 的用途。

// 🟢 好味道的收尾工作
const connection = database.connect();
try {
  // 使用 connection 進行資料庫操作
  runQuery(connection);
} catch (e) {
  log.error("Database query failed", e);
  throw e; // 繼續向上拋出
} finally {
  // 無論成功或失敗,這個區塊都會執行
  connection.close(); // 確保資料庫連線被關閉,避免資源洩漏
}

使用 finally 是確保清理工作的經典做法。
不過,針對「資源生命週期管理」這個特定問題,還有更優雅、更專門的模式,我們將在下一篇文章深入探討。

區分「預期失敗」與「非預期異常」

「處理或拋出」的決策,往往取決於你程式碼所在的架構層次

  • 非預期的異常 (Exceptions):系統故障,資料庫斷線、網路中斷,使用 throw 來處理,並讓系統快速失敗。

  • 可預期的失敗 (Return Values):業務規則的失敗,使用者輸入驗證錯誤。將「失敗」作為函式正常回傳的一部分。

// 🟢 好味道:函式清楚地承諾,它的回傳值會告訴你成功或失敗。
function validateUsername(name) {
  if (!name || name.length < 3) {
     // 這不是系統異常,是業務規則的正常分支
    return { ok: false, error: 'username_too_short' };
  }
  return { ok: true, value: name };
}

當整個團隊都遵循類似的 { ok, error, value } 格式時,你們就建立了一套一致的錯誤處理協議,大大降低了溝通成本。
https://ithelp.ithome.com.tw/upload/images/20250926/20124462vK0IzkGzLn.png

安全地崩潰:快速失敗 (Fail Fast) 的智慧

對於某些致命錯誤,例如應用程式啟動時無法連上資料庫、或是讀不到必要的設定檔,最好的策略不是「硬撐」,而是「快速失敗」。

讓程式立即崩潰聽起來很可怕,但它通常是最安全的選擇。
因為在這種核心元件失效的情況下,系統的狀態已經變得不可靠
如果讓它繼續「跛行」,它可能會在錯誤的狀態下處理請求,進而導致資料汙染、計算錯誤或更嚴重的連鎖反應。

一個乾淨的、有著清晰錯誤日誌的崩潰,遠比一個在默默汙染數據的「殭屍」系統要好得多。

錯誤處理的防臭大法

  • 絕不隱藏錯誤:空的 catch 是魔鬼。

  • 處理或拋出:一個函式的責任只有兩個:完成工作,或者清楚說明為什麼它做不到

  • 使用 finally 清理資源:確保連線、檔案等資源在任何情況下都能被釋放。

  • 區分情境:用回傳值處理可預期的失敗(業務規則),用例外處理非預期的異常(系統故障)。

  • 擁抱快速失敗:對於讓系統無法正常運作的致命錯誤,讓它立即、大聲地崩潰是最負責任的作法,這能防止更嚴重的後果。

檢查清單

  1. 我的程式碼裡是否有空的 catch 區塊?

  2. 我拋出的錯誤是否包含了足夠的上下文?是低階細節還是高階的業務錯誤?

  3. 我是否為團隊建立了一致的錯誤回傳格式?

  4. 當捕捉到錯誤時,我是否有必要的觀測手段(Log/Metrics/Trace)?

  5. 對於致命錯誤(如資料庫連線失敗),我的系統是會跛行還是快速失敗?

把錯誤處理做對,讓系統更可預期,這就是在消除你程式碼的臭味。


上一篇
Day 23- 空值處理:別回傳 null,用更安全的回應
系列文
消除你程式碼的臭味24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言