本文 AI 產出,尚未審核

TypeScript

錯誤處理與例外(Error Handling)

主題:unknown vs any in catch


簡介

在 JavaScript 中,try … catch 是捕捉執行時錯誤的唯一管道。
升級到 TypeScript 後,我們不只要抓到錯誤,更需要 正確描述錯誤的型別,才能在編譯階段得到安全感。

catch 區塊裡的變數在 TypeScript 4.0 以前會被推斷為 any,這意味著失去型別檢查的保護,容易寫出執行時會崩潰的程式碼。
從 TypeScript 4.0 起,catch 變數的預設型別改為 unknown,強迫開發者 主動做型別縮減 (type narrowing),從而提升程式的可預測性與維護性。

本文將深入比較 anyunknowncatch 中的差異,說明何時該使用哪一個,並提供實務範例、常見陷阱與最佳實踐,幫助你寫出更安全、可讀的錯誤處理程式碼。


核心概念

1. anyunknown 的基本差別

特性 any unknown
允許任意操作 ✅ 任意屬性、方法、算術運算皆可通過編譯 ❌ 需先做型別縮減才能使用屬性或方法
型別安全性 (等同於關閉 TypeScript) (保留型別檢查)
用途 快速原型、暫時性繞過 需要保留資訊但尚未確定型別的情況(例如 catch

結論:在錯誤處理的情境下,預設使用 unknown 能讓編譯器提醒你「這個錯誤到底是什麼型別?」而不是直接放行。


2. 為什麼 catch 變數預設是 unknown

try {
  // 可能拋出任何型別的錯誤
  JSON.parse('invalid json');
} catch (e) {
  // e 的型別是 unknown
  console.log(e); // ✅ 仍可輸出,但無法直接存取屬性
}
  • 保留資訊unknown 仍保留原始錯誤的所有屬性,只是編譯器不允許直接使用。
  • 強迫型別縮減:開發者必須先確認錯誤到底是 Errorstring、或自訂型別,才能安全存取。

3. 常見的型別縮減技巧

方法 說明 範例
instanceof 檢查是否為某個建構子(如 Error)的實例 if (e instanceof Error) { … }
typeof 檢查基本型別(stringnumberboolean if (typeof e === 'string') { … }
自訂型別保護 (type guard) 透過函式返回 e is MyError 讓編譯器收斂型別 function isMyError(e: unknown): e is MyError { … }
in 檢查屬性存在 確認錯誤物件是否擁有特定屬性 if ('code' in e) { … }

4. 程式碼範例

以下示範 5 個實務上常見的 catch 使用情境,從最簡單的 any 版到完整的 unknown + 型別保護版。

4.1. 直接使用 any(不建議)

// 範例 1:使用 any,編譯器不會給任何警告
try {
  // 可能拋出字串或 Error
  throw "Network error";
} catch (e: any) {
  // 直接存取屬性會編譯通過,但執行時可能失敗
  console.error(e.message); // ❗️若 e 為 string,會拋出 undefined 的錯誤
}

⚠️ 風險:若錯誤不是 Error 物件,e.message 會是 undefined,程式仍會因為 undefined 的操作而崩潰。


4.2. unknown + instanceof

// 範例 2:使用 unknown,先做 instanceof 檢查
try {
  // 可能拋出自訂錯誤
  throw new TypeError("Invalid type");
} catch (e: unknown) {
  if (e instanceof Error) {
    // 此時 e 的型別已縮減為 Error,安全存取屬性
    console.error(`Error: ${e.name} - ${e.message}`);
  } else {
    // 非 Error 型別的備案
    console.error(`非預期的錯誤: ${e}`);
  }
}

重點instanceof 是最直接的型別保護,適用於所有繼承自 Error 的錯誤物件。


4.3. unknown + typeof(處理字串或數字)

// 範例 3:錯誤可能是字串、數字或 Error
try {
  // 隨機拋出不同型別的錯誤
  const errors = [new Error("Oops"), "404 Not Found", 500];
  throw errors[Math.floor(Math.random() * errors.length)];
} catch (e: unknown) {
  if (typeof e === "string") {
    console.error(`字串錯誤訊息: ${e}`);
  } else if (typeof e === "number") {
    console.error(`錯誤代碼: ${e}`);
  } else if (e instanceof Error) {
    console.error(`Error: ${e.message}`);
  } else {
    console.error("未知錯誤類型");
  }
}

技巧typeof 只能辨識原始型別,對於自訂類別仍需 instanceof


4.4. 自訂型別保護 (type guard)

// 範例 4:自訂錯誤類別與 type guard
class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = "ApiError";
  }
}

// type guard 函式
function isApiError(e: unknown): e is ApiError {
  return e instanceof ApiError;
}

try {
  // 模擬 API 錯誤
  throw new ApiError(401, "Unauthorized");
} catch (e: unknown) {
  if (isApiError(e)) {
    // e 已被縮減為 ApiError,安全取得 status
    console.error(`API 錯誤 ${e.status}: ${e.message}`);
  } else {
    console.error("非 API 錯誤", e);
  }
}

好處:type guard 讓錯誤處理邏輯更具可讀性,且可重複使用於多個 catch 區塊。


4.5. 抽象化錯誤資訊提取的工具函式

// 範例 5:共用的錯誤訊息取得函式
function getErrorMessage(err: unknown): string {
  if (err instanceof Error) return err.message;
  if (typeof err === "string") return err;
  return JSON.stringify(err);
}

async function fetchData(url: string) {
  try {
    const resp = await fetch(url);
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    return await resp.json();
  } catch (e: unknown) {
    // 統一的錯誤訊息處理
    console.error("取得資料失敗:", getErrorMessage(e));
    // 可選:重新拋出或回傳預設值
    throw e; // 保留原始錯誤,讓上層決定如何處理
  }
}
  • 透過 單一入口 取得錯誤訊息,避免在每個 catch 裡重複 instanceof/typeof 判斷。

常見陷阱與最佳實踐

陷阱 說明 解決方案
直接存取 any 屬性 編譯器不會警告,但執行時容易 undefined 改用 unknown,或在 any 前加型別斷言 as Error(僅在確定情況下使用)
忘記檢查 Error 以外的型別 某些函式會拋出字串或自訂物件 使用 typeofin 或自訂 type guard 完整覆蓋所有可能性
過度使用 instanceof Error 若錯誤是跨執行環境(如 iframe)產生,instanceof 可能失效 可改用 e && typeof e === "object" && "message" in e 之類的結構
catch 裡直接 throw e eunknown,再拋出會失去型別資訊 使用 throw e as unknown 或在重新拋出前先做型別縮減,以保留訊息
忽略 async/await 的錯誤 await 失敗會進入 catch,但錯誤類型同樣是 unknown 同樣套用上述型別保護策略,確保每個非同步呼叫都有一致的錯誤處理

最佳實踐總結

  1. 預設使用 unknown 作為 catch 變數的型別。
  2. 立即縮減:在 catch 內第一件事就是判斷錯誤型別(instanceoftypeof、type guard)。
  3. 建立共用工具:如 getErrorMessageisApiError,減少重複程式碼。
  4. 保持錯誤原貌:除非有明確需求,否則在重新拋出錯誤時保留原始 unknown,讓上層決定如何處理。
  5. 文件化錯誤協定:大型專案應在 API 或函式說明中明確列出可能拋出的錯誤型別,讓使用者能寫出正確的型別保護。

實際應用場景

1. 前端 UI 的全域錯誤攔截

在 React、Vue 或 Angular 中,常會在根元件設置 ErrorBoundary(React)或全域的 window.onerror
使用 unknown 可確保即使第三方函式庫拋出非 Error 物件,我們仍能安全顯示友善訊息,避免 UI 整體崩潰。

function GlobalErrorHandler(e: unknown) {
  const msg = getErrorMessage(e);
  // 顯示 toast / Dialog
  showToast(`系統錯誤:${msg}`);
}
window.addEventListener("error", ev => GlobalErrorHandler(ev.error));

2. 後端 Node.js 的服務層錯誤傳遞

在 Express、Koa 等框架裡,服務層常拋出自訂錯誤(如 ValidationErrorDatabaseError)。
透過 unknown + type guard,可在中間件中統一轉換為 HTTP 狀態碼與錯誤訊息。

app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err: unknown) {
    if (isValidationError(err)) {
      ctx.status = 400;
      ctx.body = { error: err.message, fields: err.fields };
    } else if (isDatabaseError(err)) {
      ctx.status = 500;
      ctx.body = { error: "資料庫錯誤,請稍後再試" };
    } else {
      ctx.status = 500;
      ctx.body = { error: getErrorMessage(err) };
    }
  }
});

3. 共享函式庫的錯誤介面

如果你正在開發一個 npm 套件,使用者可能在不同環境呼叫你的 API。
提供 錯誤型別(如 MyLibError)並在文件中說明「此函式可能拋出 MyLibErrorError 或字串」;
在實作上,仍以 unknowncatch 變數,並在套件內部完成型別縮減,讓使用者只需要對 MyLibError 作型別保護即可。


總結

  • any 讓 TypeScript 的型別檢查失效,不適合作為 catch 變數的預設型別
  • unknown 仍保留錯誤的所有資訊,但必須先做型別縮減,才能安全存取屬性或方法。
  • 常見的縮減方式包括 instanceoftypeofin 檢查以及自訂 type guard
  • 建議在專案中統一使用 unknown,並將常用的錯誤處理邏輯抽象為工具函式,提升可讀性與維護性。
  • 在實務上,無論是前端 UI、後端服務層,或是共享的函式庫,都能藉由 unknown + 型別保護,建立一條安全且一致的錯誤傳遞管道

透過本文的概念與範例,你應該已掌握在 TypeScript 中使用 unknown 處理例外的最佳方式。未來寫程式時,只要記得 「先縮減,再使用」,就能讓錯誤處理既安全又易於維護。祝你寫程式快樂、錯誤處理無慮!