TypeScript
錯誤處理與例外(Error Handling)
主題:unknown vs any in catch
簡介
在 JavaScript 中,try … catch 是捕捉執行時錯誤的唯一管道。
升級到 TypeScript 後,我們不只要抓到錯誤,更需要 正確描述錯誤的型別,才能在編譯階段得到安全感。
catch 區塊裡的變數在 TypeScript 4.0 以前會被推斷為 any,這意味著失去型別檢查的保護,容易寫出執行時會崩潰的程式碼。
從 TypeScript 4.0 起,catch 變數的預設型別改為 unknown,強迫開發者 主動做型別縮減 (type narrowing),從而提升程式的可預測性與維護性。
本文將深入比較 any 與 unknown 在 catch 中的差異,說明何時該使用哪一個,並提供實務範例、常見陷阱與最佳實踐,幫助你寫出更安全、可讀的錯誤處理程式碼。
核心概念
1. any 與 unknown 的基本差別
| 特性 | any |
unknown |
|---|---|---|
| 允許任意操作 | ✅ 任意屬性、方法、算術運算皆可通過編譯 | ❌ 需先做型別縮減才能使用屬性或方法 |
| 型別安全性 | 無(等同於關閉 TypeScript) | 有(保留型別檢查) |
| 用途 | 快速原型、暫時性繞過 | 需要保留資訊但尚未確定型別的情況(例如 catch) |
結論:在錯誤處理的情境下,預設使用
unknown能讓編譯器提醒你「這個錯誤到底是什麼型別?」而不是直接放行。
2. 為什麼 catch 變數預設是 unknown
try {
// 可能拋出任何型別的錯誤
JSON.parse('invalid json');
} catch (e) {
// e 的型別是 unknown
console.log(e); // ✅ 仍可輸出,但無法直接存取屬性
}
- 保留資訊:
unknown仍保留原始錯誤的所有屬性,只是編譯器不允許直接使用。 - 強迫型別縮減:開發者必須先確認錯誤到底是
Error、string、或自訂型別,才能安全存取。
3. 常見的型別縮減技巧
| 方法 | 說明 | 範例 |
|---|---|---|
instanceof |
檢查是否為某個建構子(如 Error)的實例 |
if (e instanceof Error) { … } |
typeof |
檢查基本型別(string、number、boolean) |
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 以外的型別 |
某些函式會拋出字串或自訂物件 | 使用 typeof、in 或自訂 type guard 完整覆蓋所有可能性 |
過度使用 instanceof Error |
若錯誤是跨執行環境(如 iframe)產生,instanceof 可能失效 |
可改用 e && typeof e === "object" && "message" in e 之類的結構 |
在 catch 裡直接 throw e |
若 e 為 unknown,再拋出會失去型別資訊 |
使用 throw e as unknown 或在重新拋出前先做型別縮減,以保留訊息 |
| 忽略 async/await 的錯誤 | await 失敗會進入 catch,但錯誤類型同樣是 unknown |
同樣套用上述型別保護策略,確保每個非同步呼叫都有一致的錯誤處理 |
最佳實踐總結
- 預設使用
unknown作為catch變數的型別。 - 立即縮減:在
catch內第一件事就是判斷錯誤型別(instanceof、typeof、type guard)。 - 建立共用工具:如
getErrorMessage、isApiError,減少重複程式碼。 - 保持錯誤原貌:除非有明確需求,否則在重新拋出錯誤時保留原始
unknown,讓上層決定如何處理。 - 文件化錯誤協定:大型專案應在 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 等框架裡,服務層常拋出自訂錯誤(如 ValidationError、DatabaseError)。
透過 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)並在文件中說明「此函式可能拋出 MyLibError、Error 或字串」;
在實作上,仍以 unknown 為 catch 變數,並在套件內部完成型別縮減,讓使用者只需要對 MyLibError 作型別保護即可。
總結
any讓 TypeScript 的型別檢查失效,不適合作為catch變數的預設型別。unknown仍保留錯誤的所有資訊,但必須先做型別縮減,才能安全存取屬性或方法。- 常見的縮減方式包括
instanceof、typeof、in檢查以及自訂 type guard。 - 建議在專案中統一使用
unknown,並將常用的錯誤處理邏輯抽象為工具函式,提升可讀性與維護性。 - 在實務上,無論是前端 UI、後端服務層,或是共享的函式庫,都能藉由
unknown+ 型別保護,建立一條安全且一致的錯誤傳遞管道。
透過本文的概念與範例,你應該已掌握在 TypeScript 中使用 unknown 處理例外的最佳方式。未來寫程式時,只要記得 「先縮減,再使用」,就能讓錯誤處理既安全又易於維護。祝你寫程式快樂、錯誤處理無慮!