【TypeScript】エラーハンドリング完全ガイド|try/catch・unknown型・カスタムError・Result型・非同期エラーまで徹底解説

【TypeScript】エラーハンドリング完全ガイド|try/catch・unknown型・カスタムError・Result型・非同期エラーまで徹底解説 TypeScript

TypeScriptはコンパイル時の型安全性を提供しますが、実行時のエラーハンドリングこそが堅牢なアプリケーションの要です。JavaScript時代からの try/catch は使えますが、TypeScript 4.0から catch 節の変数が unknown 型になり、正しく扱わないとコンパイルエラーになります。本記事では unknown 型の絞り込みから、カスタムErrorクラス・Result型パターン・非同期エラーまで、実務で使えるTypeScriptのエラーハンドリングを完全解説します。

この記事でわかること

  • catch 節の unknown 型を安全に扱う方法
  • instanceof / typeof でエラー型を絞り込む方法
  • カスタム Error クラスの正しい型定義
  • Result型(Either型)パターンで例外を使わないエラー処理
  • async/await での非同期エラーハンドリング
  • never を使ったexhaustive checkの実装
  • useUnknownInCatchVariables オプションの使い方
スポンサーリンク

1. catch節の unknown 型 — TypeScript 4.0以降の変化

TypeScript 4.0以前は、catch 節の変数は暗黙的に any 型でした。TypeScript 4.0以降、tsconfig.json"useUnknownInCatchVariables": truestrict モードで自動有効)を設定すると、catch (e)eunknown 型になります。これにより、エラー変数を使う前に型を確認することが強制されます。

// TypeScript 4.0以前(any型): プロパティアクセスが型チェックなしで通る
try {
  riskyOperation();
} catch (e) {
  console.log(e.message); // any型なのでエラーにならないが危険
}

// TypeScript 4.0以降(useUnknownInCatchVariables: true)
try {
  riskyOperation();
} catch (e) {
  // e は unknown 型 → プロパティアクセス前に型チェック必須
  console.log(e.message); // ❌ コンパイルエラー: Object is of type 'unknown'
}

1-1. エラー変数を安全に使う基本パターン

unknown 型の e を使うには、事前に型を絞り込む必要があります。最もシンプルなパターンは instanceof Error で確認することです。

try {
  riskyOperation();
} catch (e) {
  if (e instanceof Error) {
    // この節では e は Error 型に絞り込まれる
    console.error(e.message);   // OK
    console.error(e.stack);     // OK
    console.error(e.name);      // OK
  } else {
    // Error でない場合(throw "string" など)
    console.error("Unknown error:", String(e));
  }
}

1-2. エラーメッセージを取り出すユーティリティ関数

どんなエラーでも文字列メッセージを取り出せるユーティリティ関数を作っておくと便利です。コードベース全体で再利用でき、型安全性も保てます。

/**
 * 任意の値からエラーメッセージを取り出す
 * unknown 型の catch 節の変数に対して使う
 */
function getErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  if (typeof error === "string") return error;
  if (
    typeof error === "object" &&
    error !== null &&
    "message" in error &&
    typeof (error as Record<string, unknown>).message === "string"
  ) {
    return (error as Record<string, unknown>).message as string;
  }
  return String(error);
}

// 使用例
try {
  await fetchData();
} catch (e) {
  console.error("エラー:", getErrorMessage(e)); // 常に string を返す
}
throw する値は必ず Error インスタンスにする
JavaScriptでは throw "エラー文字列"throw 404 のように文字列・数値もthrowできますが、stack トレースが取れず原因調査が困難になります。必ず throw new Error("メッセージ")Error のサブクラスをthrowするようにしましょう。

2. カスタム Error クラスの型定義

アプリケーションのエラーを分類するために、Error を継承したカスタムエラークラスを作ります。TypeScriptでは型定義が加わるため、エラーの種類に応じたプロパティを型安全に持てます。

2-1. 基本的なカスタムErrorクラス

// 基本的なカスタムErrorクラス
class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500
  ) {
    super(message);
    // TypeScript + Babel環境での instanceof 問題を回避
    this.name = this.constructor.name;
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

// 使用例
try {
  throw new AppError("ユーザーが見つかりません", "USER_NOT_FOUND", 404);
} catch (e) {
  if (e instanceof AppError) {
    console.error(`[${e.code}] ${e.message} (HTTP ${e.statusCode})`);
    // → [USER_NOT_FOUND] ユーザーが見つかりません (HTTP 404)
  }
}

2-2. エラー種別ごとにクラスを分ける

// ベースエラークラス
class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string
  ) {
    super(message);
    this.name = this.constructor.name;
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

// 具体的なエラー種別
class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} が見つかりません`, "NOT_FOUND");
  }
}

class ValidationError extends AppError {
  constructor(
    message: string,
    public readonly field: string
  ) {
    super(message, "VALIDATION_ERROR");
  }
}

class NetworkError extends AppError {
  constructor(
    message: string,
    public readonly statusCode: number
  ) {
    super(message, "NETWORK_ERROR");
  }
}

// 型ガードで絞り込み
function handleError(e: unknown): void {
  if (e instanceof NotFoundError) {
    console.warn("404:", e.message);
  } else if (e instanceof ValidationError) {
    console.error(`検証エラー [${e.field}]: ${e.message}`);
  } else if (e instanceof NetworkError) {
    console.error(`ネットワークエラー HTTP ${e.statusCode}: ${e.message}`);
  } else if (e instanceof AppError) {
    console.error(`アプリエラー [${e.code}]: ${e.message}`);
  } else if (e instanceof Error) {
    console.error("予期しないエラー:", e.message);
  } else {
    console.error("不明なエラー:", String(e));
  }
}
Object.setPrototypeOf(this, new.target.prototype) が必要な理由
TypeScriptを target: ES5 でコンパイルすると、Error のサブクラスで instanceof が正しく動作しないバグがあります。Object.setPrototypeOf(this, new.target.prototype) をコンストラクタ内に追加することで、この問題を回避できます。target: ES2015 以降では不要ですが、安全のため付けておくことを推奨します。

3. エラー型ガード関数の実装

instanceof のほかに、型ガード関数(is 構文)を使うと再利用性の高いエラー判定ロジックを書けます。特に、カスタムプロパティを持つオブジェクトエラーを判定するときに役立ちます。

// 型ガード関数の定義
function isError(value: unknown): value is Error {
  return value instanceof Error;
}

function isAppError(value: unknown): value is AppError {
  return value instanceof AppError;
}

// APIレスポンスのエラーオブジェクトを判定する型ガード
interface ApiErrorResponse {
  error: string;
  message: string;
  statusCode: number;
}

function isApiErrorResponse(value: unknown): value is ApiErrorResponse {
  return (
    typeof value === "object" &&
    value !== null &&
    "error" in value &&
    "message" in value &&
    "statusCode" in value &&
    typeof (value as ApiErrorResponse).error === "string" &&
    typeof (value as ApiErrorResponse).message === "string" &&
    typeof (value as ApiErrorResponse).statusCode === "number"
  );
}

// 使用例
async function fetchUser(id: number) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) {
      const body = await res.json();
      if (isApiErrorResponse(body)) {
        throw new NetworkError(body.message, body.statusCode);
      }
      throw new NetworkError("APIエラー", res.status);
    }
    return await res.json();
  } catch (e) {
    if (isError(e)) throw e; // Errorはそのまま再throw
    throw new AppError(String(e), "UNKNOWN");
  }
}

4. Result型パターン — 例外を使わないエラー処理

Rustや関数型言語では「エラーも戻り値として返す」Result型(Either型)パターンが標準的です。TypeScriptでも同じパターンを実装でき、例外によるコントロールフローの複雑化を防ぎ、エラーを戻り値として型安全に扱えます。特に複数のエラーケースを持つ関数で効果的です。

4-1. シンプルなResult型の定義

// Result型の定義
type Ok<T> = { success: true; data: T };
type Err<E> = { success: false; error: E };
type Result<T, E = Error> = Ok<T> | Err<E>;

// ヘルパー関数
const ok  = <T>(data: T): Ok<T>   => ({ success: true,  data });
const err = <E>(error: E): Err<E> => ({ success: false, error });

// Result型を返す関数の例
function divide(a: number, b: number): Result<number, string> {
  if (b === 0) return err("0で割ることはできません");
  return ok(a / b);
}

// 呼び出し側
const result = divide(10, 0);
if (result.success) {
  console.log("結果:", result.data);   // data は number 型
} else {
  console.error("エラー:", result.error); // error は string 型
}

4-2. 非同期処理でのResult型

type AsyncResult<T, E = Error> = Promise<Result<T, E>>;

// APIコールをResult型で包む
async function fetchUserResult(id: number): AsyncResult<User, AppError> {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) {
      return err(new NetworkError(`HTTP ${res.status}`, res.status));
    }
    const data = await res.json() as User;
    return ok(data);
  } catch (e) {
    if (e instanceof AppError) return err(e);
    return err(new AppError(getErrorMessage(e), "FETCH_ERROR"));
  }
}

// 使用例: try/catch 不要で型安全なエラー処理
const result = await fetchUserResult(123);
if (!result.success) {
  if (result.error instanceof NetworkError && result.error.statusCode === 404) {
    console.warn("ユーザーが見つかりません");
  } else {
    console.error("エラー:", result.error.message);
  }
  return;
}
// success === true のブロックでは result.data は User 型
renderUser(result.data);

4-3. try/catch をResult型に変換するラッパー

// 同期関数をResult型に変換
function tryCatch<T>(fn: () => T): Result<T> {
  try {
    return ok(fn());
  } catch (e) {
    return err(e instanceof Error ? e : new Error(String(e)));
  }
}

// 非同期関数をResult型に変換
async function tryCatchAsync<T>(fn: () => Promise<T>): AsyncResult<T> {
  try {
    return ok(await fn());
  } catch (e) {
    return err(e instanceof Error ? e : new Error(String(e)));
  }
}

// 使用例: JSON.parse のエラーを安全に捕捉
const parsed = tryCatch(() => JSON.parse(rawInput));
if (!parsed.success) {
  console.error("JSONパースエラー:", parsed.error.message);
} else {
  processData(parsed.data);
}

// 使用例: fetch をResult型で扱う
const result = await tryCatchAsync(() => fetch("/api/data").then(r => r.json()));

5. 非同期処理のエラーハンドリング

async/await では try/catch で非同期エラーを捕捉できますが、Promise.allPromise.allSettled の扱いに注意が必要です。

5-1. async/await の基本パターン

// ✅ 正しいパターン: await を try/catch で囲む
async function loadData(): Promise<Data> {
  try {
    const res = await fetch("/api/data");
    if (!res.ok) {
      throw new NetworkError(`HTTP ${res.status}`, res.status);
    }
    return await res.json() as Data;
  } catch (e) {
    if (e instanceof NetworkError) throw e; // 既知のエラーはそのままthrow
    throw new AppError(getErrorMessage(e), "LOAD_DATA_ERROR");
  }
}

// ❌ よくある間違い: await の前に try/catch を置く
async function badPattern() {
  let data;
  try {
    const promise = fetch("/api/data"); // ここではエラーは起きない
  } catch (e) { /* catch できない */ }
  data = await promise; // await の後のエラーが catch されない
}

5-2. Promise.all と Promise.allSettled の使い分け

// Promise.all: 1つでも失敗すると全体が失敗
async function fetchAll() {
  try {
    const [users, posts] = await Promise.all([
      fetchUsers(),
      fetchPosts(),
    ]);
    return { users, posts };
  } catch (e) {
    // どちらが失敗したかは e の内容から判断
    throw new AppError(getErrorMessage(e), "FETCH_ALL_ERROR");
  }
}

// Promise.allSettled: 全件実行し、成功・失敗を個別に取得
async function fetchAllSettled(ids: number[]) {
  const results = await Promise.allSettled(
    ids.map(id => fetchUser(id))
  );

  const succeeded: User[] = [];
  const failed: { id: number; reason: string }[] = [];

  results.forEach((result, index) => {
    if (result.status === "fulfilled") {
      succeeded.push(result.value);
    } else {
      failed.push({ id: ids[index], reason: getErrorMessage(result.reason) });
    }
  });

  return { succeeded, failed };
}

5-3. AbortController を使ったタイムアウト処理

// fetchのタイムアウトエラーを型安全に処理する
class TimeoutError extends AppError {
  constructor(public readonly timeoutMs: number) {
    super(`${timeoutMs}ms でタイムアウトしました`, "TIMEOUT_ERROR");
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

async function fetchWithTimeout<T>(
  url: string,
  timeoutMs: number = 5000
): Promise<T> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const res = await fetch(url, { signal: controller.signal });
    if (!res.ok) throw new NetworkError(`HTTP ${res.status}`, res.status);
    return await res.json() as T;
  } catch (e) {
    if (e instanceof Error && e.name === "AbortError") {
      throw new TimeoutError(timeoutMs);
    }
    throw e;
  } finally {
    clearTimeout(timeoutId);
  }
}

// 使用例
try {
  const data = await fetchWithTimeout<User[]>("/api/users", 3000);
} catch (e) {
  if (e instanceof TimeoutError) {
    console.warn(`タイムアウト (${e.timeoutMs}ms)`);
  } else if (e instanceof NetworkError) {
    console.error(`ネットワークエラー HTTP ${e.statusCode}`);
  }
}

6. never を使った Exhaustive Check(網羅性チェック)

ユニオン型の全ケースを switch で処理しているかどうかを、コンパイル時に保証するテクニックが Exhaustive Check です。never 型への代入を利用してケース漏れをコンパイルエラーにします。

type AppErrorType = "NOT_FOUND" | "VALIDATION_ERROR" | "NETWORK_ERROR" | "AUTH_ERROR";

// exhaustive check 用の関数
function assertNever(value: never): never {
  throw new Error(`Exhaustive check failed: ${JSON.stringify(value)}`);
}

function getHttpStatus(errorType: AppErrorType): number {
  switch (errorType) {
    case "NOT_FOUND":        return 404;
    case "VALIDATION_ERROR": return 422;
    case "NETWORK_ERROR":    return 503;
    case "AUTH_ERROR":       return 401;
    default:
      // 全ケースが処理されていれば errorType は never 型
      // 処理されていないケースがあればコンパイルエラー
      return assertNever(errorType);
  }
}

// AppErrorType に "RATE_LIMIT" を追加した場合:
// type AppErrorType = "NOT_FOUND" | ... | "RATE_LIMIT";
// → getHttpStatus の switch に "RATE_LIMIT" がないのでコンパイルエラーになる
// → ケース漏れをコンパイル時に発見できる

6-1. 判別可能なユニオン型とのExhaustive Check

type ApiError =
  | { kind: "network";    statusCode: number }
  | { kind: "validation"; fields: string[] }
  | { kind: "auth";       reason: string }
  | { kind: "notFound";   resource: string };

function formatApiError(error: ApiError): string {
  switch (error.kind) {
    case "network":
      return `ネットワークエラー (HTTP ${error.statusCode})`;
    case "validation":
      return `入力エラー: ${error.fields.join(", ")}`;
    case "auth":
      return `認証エラー: ${error.reason}`;
    case "notFound":
      return `${error.resource} が見つかりません`;
    default:
      return assertNever(error); // 全ケース網羅済みのためコンパイルエラーにならない
  }
}

判別可能なユニオン型の詳細については TypeScript 判別可能なユニオン型(Discriminated Unions)完全ガイド も参照してください。

7. エラーハンドリングに関連する tsconfig.json 設定

オプション 説明 デフォルト 推奨
useUnknownInCatchVariables catch 節の変数を unknown 型にする(strict 配下) false truestrict: true で自動有効)
strictNullChecks null/undefined を明示的に扱うことを強制 false truestrict: true で自動有効)
noUncheckedIndexedAccess 配列・オブジェクトのインデックスアクセスに undefined を含める false true(明示的に設定が必要)
strict 上記を含む厳格な型チェックをまとめて有効化 false true
// tsconfig.json 推奨設定(エラーハンドリング関連)
{
  "compilerOptions": {
    "strict": true,                      // useUnknownInCatchVariables を含む全厳格オプション
    "noUncheckedIndexedAccess": true,    // arr[0] が T | undefined になる
    "noImplicitReturns": true,           // 全パスで return を強制
    "noFallthroughCasesInSwitch": true   // switch の fallthrough を禁止
  }
}

7-1. noUncheckedIndexedAccess の効果

noUncheckedIndexedAccess を有効にすると、配列・オブジェクトへのインデックスアクセスの戻り値に undefined が含まれます。これにより、未チェックのインデックスアクセスが原因の TypeError をコンパイル時に防げます。

// noUncheckedIndexedAccess: true の効果
const items = ["apple", "banana", "cherry"];

// ❌ コンパイルエラー: items[0] は string | undefined
const first: string = items[0];  // NG

// ✅ undefined チェックが必要
const firstItem = items[0];
if (firstItem !== undefined) {
  console.log(firstItem.toUpperCase()); // OK: string に絞り込まれる
}

// ✅ ?? でデフォルト値を指定
const safeFirst = items[0] ?? "default";
console.log(safeFirst.toUpperCase()); // OK: string

// オブジェクトでも同様
const map: Record = { a: 1, b: 2 };
const val = map["a"]; // number | undefined
if (val !== undefined) {
  console.log(val * 2); // OK
}

8. エラーの再スロー・ラップ・変換パターン

8-1. エラーを別の型に変換してthrow

// 下位レイヤーのエラーを上位レイヤー向けに変換
async function getUserById(id: number): Promise<User> {
  try {
    return await db.users.findById(id);
  } catch (e) {
    // DB層のエラーをドメインエラーに変換
    if (e instanceof DatabaseNotFoundError) {
      throw new NotFoundError(`User(id=${id})`);
    }
    // 想定外のエラーはラップして再throw(原因を保持)
    throw new AppError(
      `ユーザー取得に失敗しました: ${getErrorMessage(e)}`,
      "DB_ERROR"
    );
  }
}

8-2. エラーチェーン(cause プロパティ)

// ES2022 / TypeScript 4.6以降: Error の cause プロパティ
class ServiceError extends Error {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options); // options.cause に元のエラーを渡せる
    this.name = "ServiceError";
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

async function processOrder(orderId: string): Promise<void> {
  try {
    await paymentService.charge(orderId);
  } catch (e) {
    // cause に元のエラーを保持してラップ
    throw new ServiceError(
      `注文 ${orderId} の支払い処理に失敗`,
      { cause: e }  // cause プロパティに元エラーを保持
    );
  }
}

// catch側でエラーチェーンを辿る
try {
  await processOrder("ORD-001");
} catch (e) {
  if (e instanceof ServiceError) {
    console.error("サービスエラー:", e.message);
    console.error("原因:", e.cause); // 元のエラー
  }
}

9. 実践例3本

実践例1:型安全なAPIクライアントの実装

Result型パターンとカスタムErrorクラスを組み合わせた、型安全なAPIクライアントの実装例です。エラーを戻り値として扱うため、呼び出し側でのエラー処理が型推論で補完されます。

// 型安全なAPIクライアント
class ApiClient {
  constructor(private readonly baseUrl: string) {}

  async get<T>(path: string): AsyncResult<T, NetworkError> {
    try {
      const res = await fetch(`${this.baseUrl}${path}`);
      if (!res.ok) {
        return err(new NetworkError(`GET ${path}: HTTP ${res.status}`, res.status));
      }
      return ok(await res.json() as T);
    } catch (e) {
      return err(new NetworkError(getErrorMessage(e), 0));
    }
  }

  async post<T, B>(path: string, body: B): AsyncResult<T, NetworkError | ValidationError> {
    try {
      const res = await fetch(`${this.baseUrl}${path}`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      if (res.status === 422) {
        const data = await res.json() as { field: string; message: string };
        return err(new ValidationError(data.message, data.field));
      }
      if (!res.ok) {
        return err(new NetworkError(`POST ${path}: HTTP ${res.status}`, res.status));
      }
      return ok(await res.json() as T);
    } catch (e) {
      return err(new NetworkError(getErrorMessage(e), 0));
    }
  }
}

// 使用例
const api = new ApiClient("https://api.example.com");

const result = await api.get<User[]>("/users");
if (!result.success) {
  if (result.error.statusCode === 401) {
    redirectToLogin();
  } else {
    showError(result.error.message);
  }
  return;
}
displayUsers(result.data); // result.data は User[] 型

実践例2:フォームバリデーションのエラー集約

複数フィールドのバリデーションエラーを集約してResult型で返すパターンです。全フィールドのエラーをまとめて取得でき、UIへの反映が型安全に行えます。

interface FormData {
  name: string;
  email: string;
  age: number;
}

type ValidationErrors = Partial<Record<keyof FormData, string>>;

function validateForm(data: FormData): Result<FormData, ValidationErrors> {
  const errors: ValidationErrors = {};

  if (!data.name.trim()) {
    errors.name = "名前を入力してください";
  } else if (data.name.length > 50) {
    errors.name = "名前は50文字以内で入力してください";
  }

  if (!data.email.includes("@")) {
    errors.email = "正しいメールアドレスを入力してください";
  }

  if (data.age < 0 || data.age > 150) {
    errors.age = "0〜150の数値を入力してください";
  }

  if (Object.keys(errors).length > 0) {
    return err(errors);
  }
  return ok(data);
}

// 使用例
const validation = validateForm({ name: "", email: "invalid", age: -1 });
if (!validation.success) {
  const { name, email, age } = validation.error;
  // 各フィールドのエラーメッセージを表示
  if (name)  showFieldError("name", name);
  if (email) showFieldError("email", email);
  if (age)   showFieldError("age", age);
} else {
  submitForm(validation.data); // validation.data は FormData 型
}

実践例3:エラーログ収集ミドルウェア

Express.jsのエラーハンドリングミドルウェアで、カスタムErrorクラスに基づいてHTTPレスポンスを構築し、ログ収集まで行う実践パターンです。(前提: npm install expressnpm install -D @types/express を実行済み)

import { Request, Response, NextFunction } from "express";

// Expressエラーハンドリングミドルウェア
export function errorHandler(
  error: unknown,
  req: Request,
  res: Response,
  _next: NextFunction
): void {
  // エラーの種別に応じてHTTPレスポンスを構築
  if (error instanceof ValidationError) {
    res.status(422).json({
      error: "VALIDATION_ERROR",
      message: error.message,
      field: error.field,
    });
    return;
  }

  if (error instanceof NotFoundError) {
    res.status(404).json({
      error: "NOT_FOUND",
      message: error.message,
    });
    return;
  }

  if (error instanceof AppError) {
    // 既知のアプリエラー: 4xx/5xx をログに記録してレスポンス
    console.error(`[AppError] ${error.code}: ${error.message}`);
    res.status(500).json({
      error: error.code,
      message: error.message,
    });
    return;
  }

  // 想定外のエラー: 詳細をログに残してジェネリックな500を返す
  const message = getErrorMessage(error);
  console.error(`[UnhandledError] ${req.method} ${req.path}: ${message}`);
  if (error instanceof Error) {
    console.error(error.stack);
  }

  res.status(500).json({
    error: "INTERNAL_SERVER_ERROR",
    message: "サーバーエラーが発生しました",
  });
}

10. まとめ:TypeScriptエラーハンドリング チートシート

場面 パターン ポイント
catch 節でエラーを使う if (e instanceof Error) unknown 型を絞り込んでから使う
エラーメッセージを取り出す getErrorMessage(e) ユーティリティ関数 Error・文字列・オブジェクト全ケースに対応
アプリ固有のエラー分類 class AppError extends Error Object.setPrototypeOfinstanceof を正しく動かす
例外を使わずエラーを返す Result<T, E> ok(data) / err(error) のヘルパー関数で扱いやすく
非同期エラーの一括処理 Promise.allSettled 全件実行して成功・失敗を個別に取得
ケース漏れを防ぐ assertNever(x: never) switch の全ケースを網羅したかコンパイル時に確認
エラーの原因を保持 new Error(msg, { cause: e }) TypeScript 4.6以降 / ES2022 の cause プロパティ
非同期タイムアウト AbortController + setTimeout AbortErrorTimeoutError に変換

FAQ

QuseUnknownInCatchVariables を有効にしたら既存コードが大量にエラーになりました。移行方法は?

Acatch (e)e に対するプロパティアクセスに型エラーが出ます。急ぎの場合は catch (e: any) と明示的に any を指定して一時回避できますが、根本解決は if (e instanceof Error) で絞り込むか、getErrorMessage(e) のようなユーティリティ関数を使う方法を採用してください。新規コードから段階的に修正することを推奨します。

QResult型パターンを使うと throw しないのでスタックトレースが残りませんか?

AResult型を返す関数の内部では try/catchError インスタンスをキャッチして返すため、スタックトレースは result.error.stack に保持されています。エラーを握りつぶさずに err(new Error(...)) の形でErrorインスタンスを保持すれば、後でスタックトレースを取り出せます。

QカスタムErrorクラスで instanceof が false になることがあります。

ATypeScriptを target: ES5 でコンパイルすると、Error を継承したクラスで instanceof が正しく動作しません。コンストラクタ内に Object.setPrototypeOf(this, new.target.prototype) を追加してください。target: ES2015 以上に変更することでも解決できます。

Qtry/catch と Result型はどちらを使うべきですか?

A用途によって使い分けます。「発生が稀な予期しないエラー」や「プログラムの続行が不可能なエラー」には try/catch、「業務ロジックで発生しうる想定内のエラー」(バリデーション失敗・リソース未発見など)にはResult型が適しています。APIの境界やサービス層ではResult型を使うと呼び出し側のエラーハンドリングが型安全になり、エラーを無視した実装をコンパイル時に防げます。

Qfinally は async/await でも正しく動きますか?

Aはい。async/awaittry/catch/finally は同期処理と同じように動作します。finally 節は returnthrow・正常終了のいずれでも必ず実行されます。ただし finally 内で returnthrow をすると、trycatch の戻り値やエラーが上書きされるため、finally はリソース解放(clearTimeout・接続クローズ等)のみに使うのが原則です。

QTypeScriptのエラーと関係ある内部リンク記事はありますか?

A型の絞り込み(Type Narrowing)ではinstanceoftypeof・ユーザー定義型ガードを、判別可能なユニオン型ではエラー種別の型定義パターンを詳しく解説しています。また、非同期処理の型定義ではPromiseasync/await の型についても解説しています。