本文 AI 產出,尚未審核

TypeScript 異步與 Promise:Error 型別推論


簡介

在 JavaScript 裡,throwtry / catch 是處理非同步錯誤的核心機制。當我們把程式碼搬到 TypeScript 時,除了要考慮執行時的例外外,還必須思考 型別

  • catch 區塊接收到的值到底是什麼型別?
  • 我們如何在不失去型別安全的前提下,取得錯誤的詳細資訊?

如果忽視這些問題,最常見的結果就是 anyunknown 的濫用,讓編譯器失去保護力,甚至在執行階段產生難以偵測的錯誤。本文將從 Error 型別推論 的角度,說明在 Async / Promise 流程中,如何正確、有效地取得錯誤資訊,並以實務範例展示最佳寫法。


核心概念

1. 為什麼要推論 Error 型別

在 TypeScript 4.0 之後,catch 區塊的變數預設型別從 any 變成 unknown,這是為了強迫開發者自行斷言或檢查unknown 雖然安全,但如果不加以處理,就會讓錯誤訊息「看不見」:

try {
  await fetchData();
} catch (e) {
  // e 為 unknown,直接使用 e.message 會編譯錯誤
  console.error(e.message); // ❌ TypeScript error
}

因此,我們需要 unknown 轉換成更具體的型別(如 Error、自訂錯誤類別),才能安全地存取 messagestack 或自訂屬性。

2. 基本的 Promise 錯誤處理

以下示範最簡單的 async / await + try / catch 寫法,並使用 型別斷言 讓編譯器知道錯誤是 Error

async function getUser(id: number): Promise<User> {
  const resp = await fetch(`/api/users/${id}`);
  if (!resp.ok) throw new Error(`User ${id} not found`);
  return resp.json();
}

async function showUser(id: number) {
  try {
    const user = await getUser(id);
    console.log('User:', user);
  } catch (e) {
    // 斷言 e 為 Error
    const err = e as Error;
    console.error('❗️ 取得使用者失敗:', err.message);
  }
}

要點

  • 使用 as Error 直接斷言是最直接的方式,但若錯誤來源不一定是 Error(例如拋出字串),會失去安全性。

3. unknown vs any:安全的錯誤型別檢查

為了避免過度斷言,我們可以寫一個 type guard,將 unknown 轉為 Error

function isError(value: unknown): value is Error {
  return value instanceof Error;
}

async function updateProfile(data: Profile) {
  try {
    await api.update(data);
  } catch (e) {
    if (isError(e)) {
      // 此時 e 已被縮小為 Error
      console.warn('API 錯誤:', e.message);
    } else {
      // 不是 Error,可能是字串或自訂物件
      console.warn('未知錯誤類型:', e);
    }
  }
}

重點

  • instanceof Error 能辨識原生的 Error 以及繼承自 Error 的自訂類別。
  • e 不是 Error 時,我們仍保留 unknown,避免隱藏潛在問題。

4. 自訂 Error 類別與型別推論

在大型專案中,常會自訂錯誤類別(例如 HttpErrorValidationError),此時 type guard 需要更精細:

class HttpError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = 'HttpError';
  }
}

function isHttpError(value: unknown): value is HttpError {
  return value instanceof HttpError;
}

async function fetchWithAuth(url: string) {
  try {
    const resp = await fetch(url);
    if (!resp.ok) throw new HttpError(resp.status, 'Request failed');
    return await resp.json();
  } catch (e) {
    if (isHttpError(e)) {
      // 可以直接取得 status
      console.error(`HTTP ${e.status}: ${e.message}`);
    } else {
      console.error('非 HTTP 錯誤:', e);
    }
  }
}

這樣的寫法讓 IDE 能正確提示 e.status,同時保留型別安全。

5. Async 函式的返回型別與錯誤推論

async 函式永遠回傳 Promise<T>,即使在 catch 內部重新拋出錯誤,外部仍會得到 Promise<T>,錯誤會被 Reject。若我們想在函式簽名中表達「可能會拋出 HttpError」的資訊,雖然 TypeScript 本身沒有內建的 Promise<…, E>,但可以透過 泛型 搭配 Result 型別自行描述:

type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };

async function safeFetch(url: string): Promise<Result<any, HttpError>> {
  try {
    const resp = await fetch(url);
    if (!resp.ok) throw new HttpError(resp.status, 'Network error');
    const data = await resp.json();
    return { ok: true, value: data };
  } catch (e) {
    if (isHttpError(e)) return { ok: false, error: e };
    // 其他錯誤包成 generic Error
    return { ok: false, error: e as Error };
  }
}

// 使用端
(async () => {
  const result = await safeFetch('/api/items');
  if (result.ok) {
    console.log('資料:', result.value);
  } else {
    console.error('取得失敗:', result.error.message);
  }
})();

透過 Result 型別,我們在 呼叫端 明確知道錯誤的型別,而不必在 catch 中再次做型別判斷。


常見陷阱與最佳實踐

陷阱 說明 解決方案
直接拋出字串或物件 throw "Oops"throw { code: 123 } 會使 catch 變成 unknown,失去 Error 的屬性。 只拋出 Error 或其子類別,必要時自行包裝資訊。
catch 中使用 any catch (e: any) 把安全檢查全部關閉,容易誤用。 使用預設的 unknown,搭配 type guard 逐層縮小。
忘記 await async 函式內忘記 await,錯誤會直接變成未處理的 Promise Rejection。 確保每個返回 Promise 的呼叫都有 await(或 .catch)。
finally 中重新拋錯 finally 內的錯誤會 覆寫 先前的錯誤,導致原始錯誤資訊遺失。 只在 finally 做清理工作,若需要拋錯,先保存原始錯誤。
忽略自訂錯誤的屬性 自訂錯誤類別有額外屬性(如 status),但在 catch 中只使用 e.message 使用對應的 type guard,取得全部自訂屬性。

最佳實踐

  1. 統一錯誤類別:在專案根目錄建立 errors.ts,集中管理所有自訂錯誤。
  2. 使用 type guard:每個自訂錯誤都提供 isXError 函式,讓 catch 內部保持乾淨。
  3. 避免 any:除非真的無法避免,否則堅持使用 unknown + 類型縮小。
  4. 錯誤資訊串接:在拋錯前加入上下文(如 API 路徑、參數),方便除錯。

實際應用場景

1. 前端 UI 錯誤訊息顯示

在 React + TypeScript 專案中,常見的需求是「根據錯誤類別顯示不同的 toast」:

import { isHttpError } from './errors';

async function submitForm(data: FormData) {
  try {
    await api.save(data);
    toast.success('儲存成功');
  } catch (e) {
    if (isHttpError(e) && e.status === 401) {
      toast.error('未授權,請重新登入');
    } else if (e instanceof Error) {
      toast.error(e.message);
    } else {
      toast.error('未知錯誤,請稍後再試');
    }
  }
}

透過 isHttpError,UI 能直接根據 HTTP 狀態碼 做客製化回饋。

2. Node.js 後端服務的統一錯誤處理

在 Express + TypeScript 中,我們可以把所有路由的錯誤轉成 Result,最後交給全域錯誤中介軟體:

// middleware/resultHandler.ts
import { Request, Response, NextFunction } from 'express';
import { Result } from './result';

export function resultHandler(
  req: Request,
  res: Response,
  next: NextFunction
) {
  // 假設所有路由都回傳 Promise<Result>
  (req as any).result
    .then((result: Result<any, Error>) => {
      if (result.ok) {
        res.json(result.value);
      } else {
        // 統一回傳錯誤格式
        res.status(500).json({ error: result.error.message });
      }
    })
    .catch(next); // 未捕獲的錯誤交給 Express default handler
}

路由:

router.get('/items', async (req, res, next) => {
  (req as any).result = safeFetch('/api/items');
});

這樣的設計把 錯誤型別推論 前置於服務層,讓最終的錯誤回應保持一致。


總結

  • Error 型別推論 是保護 TypeScript 程式碼在非同步環境中不被隱藏例外的關鍵。
  • unknown 開始,使用 type guard 把錯誤縮小為 ErrorHttpError 或其他自訂類別,才能安全存取 messagestack、自訂屬性。
  • 自訂錯誤類別 搭配 instanceof 判斷,可讓 IDE 提供完整的屬性提示,提升開發效率。
  • 若想在函式簽名中表達錯誤型別,Result<T, E> 之類的泛型封裝是一個實用的方案,讓呼叫端在編譯期即得知可能的錯誤。
  • 避免常見陷阱(拋出非 Error、使用 any、忽略自訂屬性),並遵循 統一錯誤類別、type guard、錯誤資訊串接 的最佳實踐。

掌握了這套 Error 型別推論 的思維後,你的 TypeScript 非同步程式碼將更加可預測、易除錯、且具備良好的型別安全,在前端 UI、Node.js 後端甚至跨平台的全端專案中,都能發揮巨大的價值。祝你寫程式開心,錯誤處理更順手!