在 TypeScript 專案裡,錯誤處理常常是最鬆的地方:
catch 到的 err 永遠是 any
throw,前端只好亂猜內容今天我們用三招,把錯誤處理變得 可預期、可補全、可檢查。
假設後端用 Express(Day 15–18 範例),我們先定義一個錯誤回應型別:
// src/types/api.ts
export type ApiError = {
  code: string;       // 錯誤代碼,例如 "USER_NOT_FOUND"
  message: string;    // 錯誤描述
  details?: unknown;  // 可選,放更多資訊
};
在 Express 中統一處理:
// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from "express";
import type { ApiError } from "../types/api";
export function errorHandler(err: any, req: Request, res: Response, next: NextFunction) {
  console.error(err);
  const apiError: ApiError = {
    code: err.code || "INTERNAL_ERROR",
    message: err.message || "Something went wrong",
    details: err.details,
  };
  res.status(err.status || 500).json(apiError);
}
在路由中用 throw:
router.get("/:id", async (req, res, next) => {
  try {
    const user = await prisma.user.findUnique({ where: { id: req.params.id } });
    if (!user) {
      const err = new Error("User not found");
      (err as any).code = "USER_NOT_FOUND";
      (err as any).status = 404;
      throw err;
    }
    res.json(user);
  } catch (e) {
    next(e);
  }
});
Result 型別模式(推薦)用 Result 讓成功與失敗用 同一型別 表示:
// src/types/result.ts
export type Result<T, E> =
  | { ok: true; value: T }
  | { ok: false; error: E };
後端 Service 層:
import type { Result } from "../types/result";
import type { ApiError } from "../types/api";
import { prisma } from "../lib/prisma";
import type { User } from "@prisma/client";
export async function getUser(id: string): Promise<Result<User, ApiError>> {
  const user = await prisma.user.findUnique({ where: { id } });
  if (!user) {
    return {
      ok: false,
      error: { code: "USER_NOT_FOUND", message: "User not found" },
    };
  }
  return { ok: true, value: user };
}
好處:
if (res.ok) 分支假設 API 錯誤統一為 ApiError 格式,前端就可以這樣處理:
import type { ApiError } from "../types/api";
import type { User } from "../types/user";
async function fetchUser(id: string): Promise<User | ApiError> {
  const res = await fetch(`/users/${id}`);
  if (!res.ok) {
    return (await res.json()) as ApiError;
  }
  return (await res.json()) as User;
}
async function showUser(id: string) {
  const result = await fetchUser(id);
  if ("code" in result) {
    // result 是 ApiError
    console.error(result.code, result.message);
  } else {
    // result 是 User
    console.log(result.name);
  }
}
避免 API 格式被後端或網路異常搞壞,可以用 zod 在前端驗證:
import { z } from "zod";
const userSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});
const apiErrorSchema = z.object({
  code: z.string(),
  message: z.string(),
  details: z.any().optional(),
});
async function fetchUserSafe(id: string) {
  const res = await fetch(`/users/${id}`);
  const json = await res.json();
  if (!res.ok) {
    return apiErrorSchema.parse(json); // 失敗就丟錯
  }
  return userSchema.parse(json);
}
這樣:
User 型別,且已驗證Result + zod 終極型別安全type UserResult = Result<
  z.infer<typeof userSchema>,
  z.infer<typeof apiErrorSchema>
>;
async function fetchUserResult(id: string): Promise<UserResult> {
  const res = await fetch(`/users/${id}`);
  const json = await res.json();
  if (!res.ok) {
    return { ok: false, error: apiErrorSchema.parse(json) };
  }
  return { ok: true, value: userSchema.parse(json) };
}
// 呼叫方:
const result = await fetchUserResult("abc");
if (result.ok) {
  console.log(result.value.name);
} else {
  console.error(result.error.code);
}
Error Class 型別化
class AppError extends Error {
  constructor(
    public code: string,
    public status: number,
    public details?: unknown
  ) {
    super(code);
  }
}
錯誤碼列舉
export enum ErrorCode {
  UserNotFound = "USER_NOT_FOUND",
  EmailExists = "EMAIL_EXISTS",
}
HTTP 錯誤碼與型別對應
type HttpStatus = 400 | 401 | 403 | 404 | 500;