第5章:catchは基本「unknown」だと思おう🛡️😳
この章のゴール🎯✨
catchで受け取る値を「信用しない」クセをつける🙅♀️unknownを 安全に 扱うための “型ガード” が書けるようになる🧠🔎- どんな変な
throwが来ても、アプリの中では Errorに寄せて扱える(正規化)ようになる🧼🧺
1) なんで unknown 前提なの?🤔💥
TypeScript/JavaScriptの世界では、throw で なんでも投げられます🎲
Error だけじゃなく、こういうのも普通に来ます👇
throw "やばい"(文字列)🧵throw 404(数値)🔢throw { message: "失敗", code: "X" }(オブジェクト)📦throw null(えっ…)😇
だから catch (e) の e を Error だと思い込むと、catch の中でさらに落ちる事故が起きます💣
(「エラー処理がエラーを起こす」やつ…最悪〜😭)
2) catch を “自動で unknown” にする設定🧷✨
TypeScriptには catch 変数を unknown 扱いにするオプションがあります✅
名前は useUnknownInCatchVariables です。(typescriptlang.org)
しかも最近のTSでは、strict を有効にしていると 既定で true 扱い(= catchがunknown)になりやすいです。(typescriptlang.org)
tsconfig.json のイメージ👇
{
"compilerOptions": {
"strict": true,
"useUnknownInCatchVariables": true
}
}
ポイント💡
- これを入れると
catch (err)のerrがunknownになるので、触る前に型チェックが必須になります🧯
3) unknown を安全に触る「型ガード」基本セット🧤🧠
3-1) まずは王道:instanceof Error 👑
try {
// 何かする
} catch (err) {
if (err instanceof Error) {
console.log(err.name, err.message);
} else {
console.log("Errorじゃないものが投げられた:", err);
}
}
これだけで “だいぶ安全” 😊👍
3-2) 「Errorっぽい」オブジェクトも拾いたい📦🔎
APIライブラリや自作コードが { message: "..." } みたいな形で投げることもあります。
message を読みに行く前にチェックしよ〜🙌
export function hasStringMessage(x: unknown): x is { message: string } {
return (
typeof x === "object" &&
x !== null &&
"message" in x &&
typeof (x as any).message === "string"
);
}
3-3) 文字列/数値/その他の “投げられがち” を拾う🎣✨
export function isPrimitiveThrowable(x: unknown): x is string | number | boolean | bigint | symbol {
const t = typeof x;
return t === "string" || t === "number" || t === "boolean" || t === "bigint" || t === "symbol";
}
4) いちばん大事:unknown → Error に「正規化」しよう🧼🧺
アプリの中では、エラーをなるべく 同じ形(Error) で扱えるとラクです😊 そのために “正規化関数” を1個用意しちゃうのが超おすすめ💖
4-1) cause で元のエラーも保持しよう🧵🎁
new Error("...", { cause: err }) の cause は ES2022 の仕組みで、原因をチェーンできます。(GitHub)
4-2) 正規化関数 toError の例🛠️✨
export function toError(thrown: unknown, fallbackMessage = "Unknown error thrown"): Error {
// 1) すでにErrorならそのまま
if (thrown instanceof Error) return thrown;
// 2) よくあるプリミティブは文字列化してErrorへ
if (typeof thrown === "string") return new Error(thrown);
if (typeof thrown === "number" || typeof thrown === "boolean" || typeof thrown === "bigint" || typeof thrown === "symbol") {
return new Error(String(thrown));
}
// 3) null/undefined は事故りやすいので固定文言で
if (thrown === null || thrown === undefined) {
return new Error(fallbackMessage);
}
// 4) message を持つ “それっぽい物” は message を採用(原因はcauseへ)
if (hasStringMessage(thrown)) {
return new Error(thrown.message, { cause: thrown });
}
// 5) 最後の手段:読める形にしてErrorへ(causeで現物は保持)
try {
return new Error(JSON.stringify(thrown), { cause: thrown });
} catch {
return new Error(fallbackMessage, { cause: thrown });
}
}
コツ💡
- ユーザー表示の文言と、ログ用の情報は分けようね🙈🔒
JSON.stringify(thrown)はログでは便利だけど、表示文言に混ぜると危険なことがあります💦
5) catchの中で「やっちゃダメ」あるある🙅♀️💥
❌ そのまま err.message を読む
err が string だったら即死です😇
❌ 何もせず握りつぶす
「なかったこと」にすると、後で調査できません😭 最低でも「正規化→ログ」まではやる癖を💪✨
6) おまけ:Promise の .catch(e => ...) も油断しない⚡😱
try/catch とは別に、Promiseの .catch の引数も “何でも来る” ので同じ発想が必要です🧠
これを助けるルールとして、typescript-eslint の use-unknown-in-catch-callback-variable があります。(TypeScript ESLint)
7) ミニ演習📝💖(手を動かすよ〜!)
演習A:わざと “変なthrow” を投げてみる🎲
throw "oops"throw { message: "だめでした", code: "X01" }throw null
→ それを catch で受けて、必ず toError に通してから message を表示してみてね😊🧼
イメージ👇
try {
throw { message: "だめでした", code: "X01" };
} catch (e) {
const err = toError(e);
console.log("OK: message取れた👉", err.message);
console.log("causeある?👉", "cause" in err ? (err as any).cause : undefined);
}
演習B:自分の “正規化ルール” を文章化📝✨
- 「ユーザーに見せる文言は短く、原因はログに」
- 「Errorじゃない throw は Error に寄せる」
- 「cause で現物を保持する」 …みたいに、3〜5行でOKだよ〜☺️🌷
8) AI活用コーナー🤖💬(めっちゃ相性いい!)
コピペで使えるプロンプト例👇✨
- 「
unknownをErrorに正規化する関数を、causeも使って提案して。想定ケースを10個出してテスト観点も添えて」🤖🧪 - 「この
hasStringMessage型ガード、抜けがないか反例を出して改善案を3つ」🔎✨ - 「Promiseの
.catch(e => ...)でも同じ安全性を保つ書き方を、lintルール込みで教えて」⚡🧷
まとめ🎀✨
catchは unknown前提が安全🛡️😳- 触る前に 型ガード!
instanceof Errorから始めよう👑 - アプリ内では unknown→Errorに正規化すると、設計が一気にラクになる🧼🧺
causeで原因を保持して、調査しやすくしよう🧵🔎(GitHub)
次の章(第6章)で、async/await の「try/catch効かない!?」系の事故に突っ込んでいくと、さらに強くなれるよ〜⚡😱💪