動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

【TypeScript】C# の try-catch 感覚で詰まるエラー処理(catch の e が unknown 型)

バイブス父さん
現役の業務SE
2026年9月16日14 min read広告 (PR) を含む場合があります
【TypeScript】C# の try-catch 感覚で詰まるエラー処理(catch の e が unknown 型)

みなさんこんにちは、ヒロポンです!

C# でずっと try { } catch (Exception ex) { log(ex.Message); } を書いてきた俺が、TypeScript の案件に入って最初に手が止まったのが、このエラー処理でした。

いつもの感覚で catch (e) { console.log(e.message); }。何気なく書いた e.message に赤い波線。「え、なんで?? catch した例外のメッセージ取りたいだけなんだけど」ってなった。

犯人は e の型でした。C# の catch (Exception ex)exException 型が付く。ところが TypeScript の catch (e)eunknownなんですよね。今回はこの「C# の try-catch 感覚で詰まる TypeScript のエラー処理」を、catch の unknown と C# の例外処理を対応させながら整理していきます。

俺の体験: e.message が使えなくて固まった

最初にぶつかったのは、コンパイルエラーの TS18046

try {
  doSomething();
} catch (e) {
  console.log(e.message); // ❌ error TS18046: 'e' is of type 'unknown'.
}

C# なら catch (Exception ex)ex.Message は当たり前に読める。だから TypeScript でも同じノリで書いた。で、エディタが赤線を出した瞬間に「ん?何が気に入らんの??」となった。正直、原因にたどり着くまで小一時間ハマった。

理由を知ると腑に落ちます。JavaScript は throw で何でも投げられるException を継承したオブジェクトだけじゃない。文字列でも数値でも投げられる。だから catch 側は「何が飛んでくるか分からない」= unknown として受けるのが正しい、という設計なんですよね。「投げられるのは Exception 派生だけ」という C# とは、そもそも前提が違う。

対応マップ: C# の例外処理 ↔ TypeScript のエラー処理

まず全体の対応を並べておきます。C# のどの感覚が、TypeScript のどこに化けるか。

C#の例外処理とTypeScriptのエラー処理を、catch変数の型・メッセージ取得・種類での分岐・投げられる値・独自エラーの5観点で対応させた比較表

この表の一番下が本質です。C# は型で守られてる。TypeScript は catch した後に自分で型を確かめるのが仕事になる。どっちが上とかじゃなく、流儀が違うだけ。では対応を1つずつ見ていきます。

対応1: catch の変数 — Exception ↔ unknown

C# は catch のカッコで型を書きます。

// C#
try {
    DoSomething();
} catch (Exception ex) {
    Console.WriteLine(ex.Message); // ex は Exception 型
}

TypeScript は catch のカッコに型を書けない。書くと構文エラー。eunknown で固定です。だから直接 e.message は読めない。

// TypeScript
try {
  doSomething();
} catch (e) {
  // e は unknown。この時点では e.message も e.stack も触れない
}

「じゃあ型注釈を付ければいいのでは??」と思うところ。でも catch (e: Error) はコンパイルエラー。書けるのは catch (e: unknown) だけです(何もしないのと同じだけど、意図を明示したい時にはアリ)。

対応2: 型を絞る — catch (SqlException) ↔ instanceof Error

C# は catch を型ごとに並べて分岐できる。

// C#
try {
    DoSomething();
} catch (SqlException ex) {
    // DB 系
} catch (Exception ex) {
    // その他
}

TypeScript の catch は1つだけ。受けてから instanceof で絞ります。要点はこの差。

catchのeにe.messageを直接アクセスしてTS18046になる書き方から、instanceof Errorで絞ってからe.messageを読む書き方への修正

instanceof Error で絞ると、そのブロックの中では eError 型に狭まる。ここで初めて e.message が読めるようになります。

// TypeScript
try {
  doSomething();
} catch (e) {
  if (e instanceof Error) {
    console.log(e.message); // ✅ ここでは e は Error 型
  } else {
    console.log("Error 以外が投げられた:", e);
  }
}

この instanceof の絞り込みが、C# の「型別 catch」に相当します。独自エラーで分けたいなら if (e instanceof AppError) のように書く。

対応3: 何でも投げられる — throw new Exception ↔ throw anything

ここが C# 出身者の一番の落とし穴。C# の throwException 派生しか投げられない。ところが JavaScript / TypeScript はリテラルでも投げられるんですよね。

throw new Error("ちゃんとしたエラー");
throw "文字列も投げられる";  // これも合法
throw 42;                    // これも

だから catch (e) で受けた時、eError とは限らない。誰かが throw "boom" してたら、e instanceof Errorfalsee.messageundefined になる。ここを想定してないと「なぜか message が取れない」で詰まります。

安全側に倒すなら、message を安全に取り出すヘルパーを1個用意しておくと楽です。

function toMessage(e: unknown): string {
  if (e instanceof Error) return e.message;
  if (typeof e === "string") return e;
  return String(e);
}

これで catch (e) { log(toMessage(e)); } と書ける。何が飛んできても落ちない。こんな感じで unknown を1回だけ噛み砕く関数を挟むと、後がすっきりします。

対応4: 独自エラークラス — : Exception ↔ extends Error

C# で class MyException : Exception を作る感覚は、TypeScript では extends Error にあたります。

class AppError extends Error {
  constructor(message: string, public readonly code: number) {
    super(message);
    this.name = "AppError"; // これを設定しないと name が "Error" のまま
  }
}

// 使う側
try {
  throw new AppError("在庫が足りません", 409);
} catch (e) {
  if (e instanceof AppError) {
    console.log(`${e.name}(${e.code}): ${e.message}`);
  }
}

super(message) で親の Error にメッセージを渡すのは、C# の : base(message) と同じ発想。this.name を明示しないと、ログに Error としか出ない。独自エラーには付けておくといい感じになります。

ミニマム検証のハンズオン

手元で確かめるなら、error-demo.ts を作って型チェックと実行を両方かけます。

// error-demo.ts
class AppError extends Error {
  constructor(message: string, public readonly code: number) {
    super(message);
    this.name = "AppError";
  }
}

function toMessage(e: unknown): string {
  if (e instanceof Error) return e.message;
  if (typeof e === "string") return e;
  return String(e);
}

for (const thrown of [new AppError("在庫不足", 409), "生の文字列", 42]) {
  try {
    throw thrown;
  } catch (e) {
    console.log(`instanceof Error: ${e instanceof Error} / message: ${toMessage(e)}`);
  }
}

tsc --strict で型が通ることを確認して(e.message を直接書くと TS18046 で弾かれる)、実行する。すると Error 派生・文字列・数値それぞれで instanceof の結果が変わるのが目で見えます。

ハマりポイント(実体験ベース)

unknown を面倒がって as Error でキャストする

catch (e) { const err = e as Error; log(err.message); }。これ、書けてしまうんですよね。でも throw "boom" が来た瞬間、err.message は実行時に undefined。型チェックは通るのに本番で落ちる、一番タチが悪いやつ。as で握りつぶさず、instanceof で絞る。

instanceof Error は「Error 派生」しか拾わない

文字列や、ライブラリが投げるプレーンオブジェクト({ code: "ENOENT" } みたいなやつ)は instanceof Errorfalse です。instanceof だけに頼ると取りこぼす。生の値も想定して、typeofin でのチェックを足しておく。

古い ES5 ターゲットだと instanceof が効かないことがある

tsconfigtargetES5 だと、extends Error したクラスの instanceoffalse になる、有名な罠。down-level 時にプロトタイプチェーンが切れるのが原因です。この時は constructor 内で Object.setPrototypeOf(this, AppError.prototype) を足す。targetES2015 以降なら、これは気にしなくて大丈夫。

俺の現場メモ

C# の例外処理が体に染みてると、TypeScript の unknown catch は最初「なんで型付けてくれないんだよ」と感じる。でも throw に何でも来る前提を知ると、むしろ堅い設計だなと納得した。実際、現場で落ち着いたのはこのやり方でした。toMessage みたいな型ガード込みのヘルパーを1個共通化して、catch では毎回それを呼ぶ。unknown をその場その場で捌くと instanceof があちこちに散らばる。だから1箇所に寄せる。C# で共通の例外ハンドラを書く感覚に近いです。

「型で守られてる」から「自分で型を確かめる」への頭の切り替え。ここさえ越えれば、TypeScript のエラー処理は C# の知識がそのまま効きます。

まとめ

  • TypeScript の catch (e)eunknown(4.4以降)。直接 e.message は読めない。
  • if (e instanceof Error) で絞ってから e.message。これが C# の型別 catch に相当。
  • JavaScript は何でも throw できるので、instanceof Error で本当に Error か確かめる。
  • 独自エラーは extends Errorsuper(message)this.name。C# の : Exception と同じ発想。
  • as Error の握りつぶしは本番で落ちる。型ガードのヘルパーに寄せる。

C# の例外処理の知識は、翻訳さえすればそのまま TypeScript で通用します。いい感じに橋を架けていきましょう!!

よくある質問

TypeScript の catch (e) で e.message が使えないのはなぜですか?

TypeScript 4.4 以降(strict 有効時)は catch した変数が unknown 型になるためです。unknown はどんな値か不明なので、プロパティに直接アクセスできません。if (e instanceof Error)Error 型に絞ってから e.message を読みます。

C# の catch (SqlException ex) のように型で分岐するには?

TypeScript には catch の型別オーバーロードがありません。1つの catch で受けてから、if (e instanceof MyError) のように instanceof で分岐します。JavaScript は文字列や数値も throw できるので、まず instanceof Error で本当に Error か確かめるのが安全です。

TypeScript で独自の例外クラスはどう作りますか?

class AppError extends Error { } のように Error を継承し、constructor で super(message) を呼び、this.name を設定します。C# の class MyException : Exception に対応します。ES5 ターゲットのときだけ Object.setPrototypeOf の追加が要ります。

catch した値をそのまま as Error でキャストしてもいいですか?

動きはしますが、実際に Error でない値(throw された文字列など)が来た時に実行時エラーになります。as で握りつぶさず、instanceof Error の型ガードで安全に絞るのが基本です。

次に読むべき記事

以上!

同じ「C# の catch 感覚で e.message が赤線」で固まった人、どんどんシェア待ってるぜ!!


この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について