みなさんこんにちは、ヒロポンです!
C# でずっと try { } catch (Exception ex) { log(ex.Message); } を書いてきた俺が、TypeScript の案件に入って最初に手が止まったのが、このエラー処理でした。
いつもの感覚で catch (e) { console.log(e.message); }。何気なく書いた e.message に赤い波線。「え、なんで?? catch した例外のメッセージ取りたいだけなんだけど」ってなった。
犯人は e の型でした。C# の catch (Exception ex) は ex に Exception 型が付く。ところが TypeScript の catch (e) の e は unknown 型なんですよね。今回はこの「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 した後に自分で型を確かめるのが仕事になる。どっちが上とかじゃなく、流儀が違うだけ。では対応を1つずつ見ていきます。
対応1: catch の変数 — Exception ↔ unknown
C# は catch のカッコで型を書きます。
// C#
try {
DoSomething();
} catch (Exception ex) {
Console.WriteLine(ex.Message); // ex は Exception 型
}
TypeScript は catch のカッコに型を書けない。書くと構文エラー。e は unknown で固定です。だから直接 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 で絞ります。要点はこの差。

instanceof Error で絞ると、そのブロックの中では e が Error 型に狭まる。ここで初めて 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# の throw は Exception 派生しか投げられない。ところが JavaScript / TypeScript はリテラルでも投げられるんですよね。
throw new Error("ちゃんとしたエラー");
throw "文字列も投げられる"; // これも合法
throw 42; // これも
だから catch (e) で受けた時、e が Error とは限らない。誰かが throw "boom" してたら、e instanceof Error は false。e.message は undefined になる。ここを想定してないと「なぜか 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 Error が false です。instanceof だけに頼ると取りこぼす。生の値も想定して、typeof や in でのチェックを足しておく。
古い ES5 ターゲットだと instanceof が効かないことがある
tsconfig の target が ES5 だと、extends Error したクラスの instanceof が false になる、有名な罠。down-level 時にプロトタイプチェーンが切れるのが原因です。この時は constructor 内で Object.setPrototypeOf(this, AppError.prototype) を足す。target が ES2015 以降なら、これは気にしなくて大丈夫。
俺の現場メモ
C# の例外処理が体に染みてると、TypeScript の unknown catch は最初「なんで型付けてくれないんだよ」と感じる。でも throw に何でも来る前提を知ると、むしろ堅い設計だなと納得した。実際、現場で落ち着いたのはこのやり方でした。toMessage みたいな型ガード込みのヘルパーを1個共通化して、catch では毎回それを呼ぶ。unknown をその場その場で捌くと instanceof があちこちに散らばる。だから1箇所に寄せる。C# で共通の例外ハンドラを書く感覚に近いです。
「型で守られてる」から「自分で型を確かめる」への頭の切り替え。ここさえ越えれば、TypeScript のエラー処理は C# の知識がそのまま効きます。
まとめ
- TypeScript の
catch (e)のeはunknown(4.4以降)。直接e.messageは読めない。 if (e instanceof Error)で絞ってからe.message。これが C# の型別 catch に相当。- JavaScript は何でも throw できるので、
instanceof Errorで本当に Error か確かめる。 - 独自エラーは
extends Error+super(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 が赤線」で固まった人、どんどんシェア待ってるぜ!!




