我在 TypeScript 的 catch 块里顺手写了 e.message 想拿错误信息,编译器却报错说对象类型为 unknown,我一度以为是 TS 太死板,后来才明白它是在提醒我一个我从没认真想过的事实——catch 到的东西,根本不保证是个 Error 的深度复盘
这是一次让我对"从一个我控制不了的地方接来的东西,凭什么假设它一定是我以为的类型"有了刻骨认知的事故。我在 TypeScript 里写 try/catch,catch 到异常后,很自然地想拿错误信息:catch (e) { log(e.message) }。在我脑子里,catch 到的当然是个错误对象、当然有 message 属性——这不天经地义吗?
可编译器直接给我报错:Object is of type 'unknown'(对象类型为 unknown),e.message 不让访问。我一开始有点烦躁,觉得 TS 是不是太死板了——catch 个异常拿个 message 都不行?我甚至想随手 (e as Error).message 强转过去了事。可冷静下来一想、再去查清楚 TS 的设计意图,我才恍然大悟、甚至有点后怕:TS(从 4.4 起,在 useUnknownInCatchVariables 下)把 catch 到的变量类型定为 unknown,不是死板,而是在如实地告诉我一个我一直忽略的真相——在 JavaScript/TypeScript 里,throw 可以抛任何东西,不只是 Error!可以 throw "字符串"、throw 42、throw {code: 500}、throw null……所以 catch 到的 e,根本不保证是个 Error、不保证有 message。我之前在别的代码里 e.message 用得好好的,纯粹是因为那些地方碰巧抛的都是 Error;一旦某处抛了个非 Error(比如某个库 throw 了一个字符串、或一个普通对象),我的 e.message 就会拿到 undefined、甚至在后续操作里崩掉,而我还在纳闷"错误信息怎么没了"。
故障现场:catch 到的不一定是 Error,e.message 可能是 undefined
我把这个"catch 到的东西类型不定"的现象还原出来,问题一目了然:
// 我以为: catch 到的就是 Error, 有 message
try {
doSomething();
} catch (e) {
log(e.message); // ✗ TS 报错: Object is of type 'unknown'
}
// TS 为什么这么严: 因为 JS 里 throw 可以抛【任何东西】, 不只是 Error
throw new Error("正常错误"); // e 是 Error, 有 message
throw "出错了"; // e 是 string, 没有 .message
throw 42; // e 是 number
throw { code: 500 }; // e 是普通对象, 没有 .message
throw null; // e 是 null!
// 第三方库、老代码、Promise reject 非 Error... 都可能抛出非 Error 的东西
// 所以 catch 到的 e, 类型只能是 unknown —— "我不知道它是什么"
// 如果我硬当它是 Error:
catch (e) {
log((e as Error).message); // 若 e 其实是字符串/对象, message 是 undefined
// 甚至: 若 e 是 null, 后续 e.xxx 直接 TypeError 崩溃 ✗
}
// 真相: e 的类型是 unknown, 不是因为 TS 死板,
// 而是因为它【真的可能是任何东西】——TS 只是没替我假设它是 Error
看着"throw 可以抛任何东西、catch 到的真不一定是 Error",我才彻底明白:我一直默认"catch 到的就是个 Error、就有 message",可这个假设根本没有保证——JS 的 throw 能抛任何值,catch 到的 e 究竟是什么,取决于"是谁、抛了什么",而这往往是我控制不了的(第三方库、底层代码、reject 了非 Error 的 Promise)。TS 把 e 标成 unknown,正是如实地承认"我不知道它是什么类型",并强制我在使用前先确认。它不是在刁难我,而是在逼我面对一个我一直回避的不确定性:这个从外部接来的东西,我凭什么假设它是 Error?不是 TS 太死板,是我一直在对一个不确定类型的东西,做着想当然的类型假设。
第一件事:搞懂 catch 到的为何是 unknown——throw 可抛任意值
冷静下来,我去把"TS 的 catch 变量类型与异常处理"这一课认真补了,才明白这个"unknown"背后的深意:
【为什么 catch 到的是 unknown, 而不是 Error】
JavaScript 的事实: throw 可以抛【任何值】, 不限于 Error
- throw new Error(...) / throw "str" / throw 42 / throw {...} / throw null
- 第三方库、Promise reject(非 Error)、老代码……都可能抛出非 Error
因此 catch 到的变量, 类型上"无法保证是 Error":
- TS 4.4+ 在 useUnknownInCatchVariables(strict 默认开)下, 把它标为 unknown
- unknown = "我不知道它是什么"——你必须先【收窄类型】才能用它
- 这是 TS 在如实表达"运行时这里可能是任何东西"的不确定性, 不是死板
为什么不直接当 Error 用(as Error)会出问题:
- 如果实际抛的是 string/对象/null, (e as Error).message 是 undefined,
或后续解引用 null 直接崩 → 你想拿的错误信息没拿到、还可能引入新崩溃
正确处理: 使用前先"收窄"它到底是什么
- if (e instanceof Error) { e.message } ← 是 Error 才取 message
- else { String(e) } ← 不是 Error 就转成字符串兜底
- 或封装一个 toError(e): 任何东西都归一成一个 Error
- 别用 (e as Error) 硬断言——那是关掉检查、把不确定性藏起来
更广的认识:
unknown 是 TS 给"来源不可信/类型不确定"的东西的【正确类型】:
外部输入、JSON.parse 的结果、catch 的异常、any 的安全替代……
它强制你"先校验/收窄, 再使用", 而不是想当然地假设类型
这一下点醒了我:我把"catch 到的是 Error"当成了理所当然,可 JS 的 throw 能抛任何值——catch 到的东西真的可能是任何类型,这个不确定性是客观存在的。TS 把它标成 unknown,不是死板,而是诚实:它拒绝替我做"它一定是 Error"这个没有依据的假设,转而强制我在用之前先确认它到底是什么。我嫌它麻烦、想 as Error 绕过去,其实是想逃避这个不确定性——而逃避的代价,就是某天真抛了个非 Error 时,我的代码静默拿到 undefined 或直接崩。不是 TS 不让我用,是它在提醒我:你正要使用一个你并不真正了解其类型的东西,先搞清楚它是什么再说。
第二件事:正解——用前先收窄,instanceof Error 判断、否则兜底
找到根因,正解就清晰了:把 catch 到的 unknown 在使用前先收窄类型——用 e instanceof Error 判断它是不是 Error,是就放心取 e.message,不是就用 String(e) 等方式兜底转成可用的信息;或封装一个 toError(e) 把任何抛出物都归一成一个 Error。绝不用 e as Error 硬断言把不确定性藏起来。
// 错误: 直接当 Error 用 / 硬断言
catch (e) { log(e.message); } // ✗ 编译报错(unknown)
catch (e) { log((e as Error).message); } // ✗ 编译过了, 但 e 非 Error 时 message=undefined/崩
// 正解1: instanceof Error 收窄, 否则兜底
catch (e) {
if (e instanceof Error) {
log(e.message); // ✓ 确认是 Error 才取 message
} else {
log(`非 Error 异常: ${String(e)}`); // ✓ 兜底, 把任何东西转成字符串
}
}
// 正解2: 封装 toError —— 把任何抛出物归一成 Error, 后续统一处理
function toError(e: unknown): Error {
if (e instanceof Error) return e;
return new Error(typeof e === "string" ? e : JSON.stringify(e));
}
catch (e) {
const err = toError(e); // 之后 err 一定是 Error, 放心用
log(err.message);
report(err);
}
// 正解3: 同理对待一切 unknown 来源(外部输入/JSON.parse), 先校验再用
const data: unknown = JSON.parse(raw);
if (isUser(data)) { use(data); } // 类型守卫确认后才用, 别直接 as User
// 关键: unknown 强制"先确认再用"; 别用 as 把它硬转成你"以为"的类型
这套做法的精髓,是面对一个类型不确定的东西,先确认它到底是什么、再使用,而不是想当然地假设它的类型:instanceof Error 是对 catch 异常做"类型收窄"的标准方式,确认是 Error 才取 message、不是就兜底;toError 则把"归一化"这件事封装起来,让后续逻辑总能拿到一个真正的 Error。核心是用 unknown 提供的"强制收窄"来正视并处理不确定性,而不是用 as 断言把不确定性掩盖掉。不是不能用 catch 到的 e,而是用之前先搞清楚它是什么。
【处理 catch 异常 / unknown, 几条原则】
1. catch 到的是 unknown: throw 可抛任何值, 它不保证是 Error
2. 用前先收窄: if (e instanceof Error) 取 message, else 用 String(e) 兜底
3. 封装 toError(e): 把任意抛出物归一成 Error, 统一后续处理
4. 别用 (e as Error): 那是关掉检查、把"它可能不是 Error"的风险藏起来
5. 自己 throw 时, 永远 throw Error(或其子类), 别 throw 字符串/裸对象,
让下游 catch 到的更可预期
6. 一切 unknown 来源(外部输入/JSON.parse/any 替代)都"先校验/收窄再用"
第三件事:其他"对不确定来源的东西想当然假设类型"的同类坑
顺着"来源不可信的东西要先确认类型再用"这条线,我把同类的坑都梳理了一遍,它们都源于"对一个其实不确定的东西,假设了它的类型":
第一个,JSON.parse 的结果直接 as 成某类型。JSON.parse(raw) as User 不做校验,外部数据格式不对就带着错误类型流进来。该用类型守卫校验后再用。
第二个,接口响应不校验就当成约定的类型。后端返回的字段缺了/类型变了,前端直接当成预期类型用,运行时崩。要在边界校验(zod 等)。
第三个,用 any 接外部数据、关掉类型检查。把 unknown 该管的地方用 any 绕过,错误的类型一路畅通无阻。外部数据该用 unknown + 校验。
第四个,自己 throw 非 Error,坑了下游。throw "失败"、reject(对象),让下游 catch 到的没有堆栈、没有 message,排查困难。自己永远抛 Error。
第四件事:处理 catch 异常的几种写法,一张表对照
我把几种处理 catch 异常的写法在"安全性、能否拿到真实信息"上的差别整理成一张表,这是我现在写 catch 的依据:
| 写法 | 编译 | e 非 Error 时 | 评价 |
|---|---|---|---|
| e.message(直接用) | ✗ 报错(unknown) | — | TS 拦住, 提醒你收窄 |
| (e as Error).message | ✓ 过 | undefined / 后续可能崩 | 错: 藏起不确定性 |
| e instanceof Error 判断 | ✓ | 走 else 兜底 | 正解, 安全收窄 |
| toError(e) 归一 | ✓ | 转成 Error | 正解, 统一处理 |
| String(e) | ✓ | 转成字符串(总能拿到) | 简单兜底可用 |
这张表让我看清:e as Error 只是骗过编译器、把"它可能不是 Error"的风险藏起来;只有 instanceof Error 收窄、toError 归一、String(e) 兜底,才真正正视了"它可能是任何东西"并安全处理。TS 报的那个 unknown 错误,是在帮我,不是在烦我。面对不确定类型的东西,确认比假设安全得多。
第五件事:我对"catch 到的就是 Error"的几个想当然
这次事故,本质是我把"catch 到的一定是 Error"当成了理所当然。把这些想当然列出来,每一条都值得警惕:
| 我曾经的想当然 | 事故教我的真相 |
|---|---|
| "catch 到的当然是 Error, 有 message" | throw 可抛任何值, catch 到的不保证是 Error |
| "TS 报 unknown 是太死板" | 它在如实表达"这里可能是任何东西", 是诚实不是死板 |
| "(e as Error) 强转一下就行" | 只骗过编译器; e 非 Error 时 message 是 undefined/崩 |
| "我别处 e.message 一直没问题" | 那是碰巧都抛了 Error; 换个抛非 Error 的地方就翻车 |
| "unknown 没用, 不如直接 any" | unknown 强制先收窄再用, 正是 any 的安全替代 |
| "throw 字符串/对象更省事" | 坑下游: catch 到的没堆栈没 message, 难排查 |
第六件事:处理异常、处理不确定来源数据时,我现在的自检习惯
现在每当我写 catch、或处理一个来源不确定的东西,或排查"错误信息怎么是 undefined",我都会先按这张图问自己:
这张图的精髓,是"来源不确定的东西就当它是 unknown、用前先确认再用;catch 的异常先 instanceof Error 收窄、否则兜底"。写时就catch 用 instanceof Error 或 toError、外部数据先校验、别用 as 硬转、排查就看错误信息丢失/崩是不是把非 Error 当成了 Error。这套习惯,让我从"catch 到的就是 Error"变成了"来源不确定就先确认类型再用"——核心始终是:JavaScript 的 throw 可以抛任何值(不只 Error,可以是字符串/数字/对象/null),所以 catch 到的东西根本不保证是 Error;TS 把 catch 变量类型标为 unknown 不是死板而是诚实——它如实表达"这里运行时可能是任何东西"并强制你使用前先收窄;直接 (e as Error).message 只骗过编译器、e 非 Error 时 message 是 undefined 甚至崩;正解是用前先收窄——instanceof Error 判断(是取 message 否则 String(e) 兜底)、或封装 toError 把任意抛出物归一成 Error、自己 throw 时永远抛 Error,推而广之一切 unknown 来源(外部输入/JSON.parse)都先校验收窄再用、别用 as 把不确定性藏起来。
我立下的几条规矩
这场"catch 到的不一定是 Error"的事故,换来了我写 TypeScript 时,刻进骨子里的几条铁律:
- JS 的 throw 可以抛任何值,catch 到的东西不保证是 Error、不保证有 message。
- TS 把 catch 变量标为 unknown 不是死板,是如实表达不确定性、强制你用前先收窄。
- catch 异常用前先收窄:if (e instanceof Error) 取 message,否则用 String(e) 等兜底。
- 可封装 toError(e) 把任意抛出物归一成 Error,让后续统一处理。
- 别用 (e as Error) 硬断言——那只骗过编译器、把"它可能不是 Error"的风险藏起来。
- 自己 throw 永远抛 Error(或其子类),别抛字符串/裸对象,让下游 catch 更可预期。
- 推而广之:一切来源不确定的东西(外部输入、JSON.parse、any 替代)都用 unknown、先校验收窄再用。
附:我现在统一处理异常的 toError + 安全 catch 工具
这是我现在固定用的一套异常归一化工具——把这次踩坑的教训(catch 到的是 unknown、用前先收窄、归一成 Error)固化成一个 toError 和一个安全的错误信息提取函数,让 catch 块再也不用裸面对那个 unknown:
/** 把任意抛出物(unknown)归一成一个真正的 Error */
export function toError(e: unknown): Error {
if (e instanceof Error) return e; // 本来就是 Error, 直接用
if (typeof e === "string") return new Error(e);
try {
return new Error(JSON.stringify(e)); // 对象等 → 序列化进 message
} catch {
return new Error(String(e)); // 兜底(循环引用等)
}
}
/** 安全取错误信息: 任何 unknown 都能拿到一段可读的描述, 不会崩 */
export function errorMessage(e: unknown): string {
return toError(e).message;
}
// 用法: catch 里不再纠结 e 是什么, 统一走工具
try {
await doSomething();
} catch (e) {
const err = toError(e); // err 一定是 Error, 有 message 有 stack
logger.error(err.message, { stack: err.stack });
report(err); // 上报也拿到了一个规范的 Error
}
// 还可配 ESLint 规则 @typescript-eslint/no-unsafe-* + 禁止 (e as Error),
// 从源头挡住"对 unknown 想当然断言"的写法
这套工具把我这次的教训钉死在了一个统一入口:无论 throw 出来的是 Error、字符串、对象还是 null,toError 都把它归一成一个真正的、带 message 的 Error;catch 块只管调它,再不用裸面对那个 unknown、也不用赌"它是不是 Error"。有了它,我的错误日志里永远有一段可读的 message、一份可用的 stack,不会再出现"错误信息是 undefined"或"处理异常时自己又崩了"。把"来源不确定的东西用前先确认并归一"这个道理,沉淀成一个所有 catch 都走的工具,这是我对这次事故最实在的交代——毕竟,处理错误的代码本身,是最不该因为一个想当然的类型假设而再出错的地方。
这件事过后,我把项目里所有 catch 块都过了一遍,凡是直接 e.message、或 (e as Error) 的统统改成走 toError,还在 ESLint 里禁了对 unknown 的裸断言。改完跑了一圈,顺手揪出两处下游库会抛字符串、原来一直被我当 Error 处理而悄悄丢了信息的地方。那种把一堆想当然的类型假设逐一落实成显式确认的踏实,是这次被 TS 拦一下换来的最实在的回报——我一开始还嫌它烦,后来真心觉得它是在替我兜底。
更深一层,这件小事改变了我对类型报错的态度。以前编译器拦我,我第一反应是嫌它碍事、想办法绕过去;现在我会先停下来问一句:它拦我,是不是因为我正在做一个没有依据的假设?很多时候,那个让我不舒服的红线,恰恰是在替我挡住一个我自己没看见的坑。把编译器当成爱挑刺的对手,还是当成帮我兜底的伙伴,写出来的代码稳健程度天差地别。
我也借这次机会在团队里立了条小约定:自己 throw 永远抛 Error 或其子类,catch 一律走 toError,禁止对 unknown 裸断言。一条小约定,挡掉的可能是日后某个深夜对着一句 undefined 的错误信息束手无策的排查。把自己踩过的坑沉淀成别人不必再踩的规则,大概是复盘最值得做的那部分。
说到底,这次不过是被一条红线拦了一下、加了个 instanceof 判断,可它让我真正记住的,是别再对自己控制不了来源的东西想当然。catch 的异常、外部的输入、第三方的返回——它们都不归我管,我唯一能做的,就是在用它们之前,诚实地承认我并不知道它们是什么,然后先确认一下。这份诚实,比任何聪明的断言都更让代码经得起折腾。
写在最后
回头看,这场由"catch 到的是 unknown"引发的"错误信息丢失"事故,真正教给我的,远不止"用 instanceof Error 收窄"这一个技巧。它让我对"我们太习惯于对'从外部接来的、自己控制不了来源的东西', 想当然地假设它'就是我以为的那个样子'; 可'我以为'和'它实际是什么'之间, 隔着一道我们常常视而不见的鸿沟——而一个诚实的工具, 与其替我们假设、给我们虚假的安心, 不如老老实实地说'我不知道它是什么, 你用之前自己先确认'",有了一次刻骨的体会。我栽跟头(差点),是因为我对一个'来源不受我控制、内容不被保证'的东西, 做了一个'它一定符合我的预期'的假设——我默认 catch 到的就是 Error, 就像我默认很多外部数据就是我约定的格式一样;我没意识到, "抛什么"是由别人(底层代码、第三方库)决定的, 我无权假设它一定是 Error; TS 把它标成 unknown, 恰恰是在拒绝替我做这个没有依据的假设;我一开始还嫌它"死板"、想用 as 绕过去——可那其实是我想逃避"我并不真正知道它是什么"这个事实, 用一句空头断言换一份虚假的安心。这让我领悟到一个关于"不确定性、诚实与假设"的深刻认知:对于"来源不受自己控制、内容不被保证"的东西, "诚实地承认'我不知道它是什么'" 远比 "想当然地假设'它就是我以为的'" 安全; 前者会逼你去确认, 后者则在埋雷;一个好的工具/类型系统/契约, 它的价值之一, 恰恰是"不替你做没有依据的假设"——它宁可用一个"未知"的标记让你不舒服、逼你显式处理, 也不给你一个"看似确定、实则虚假"的安心;而当我们嫌这种"逼我确认"麻烦、想用强制断言绕过去时, 我们绕过的不是工具的死板, 而是现实本身的不确定性——它不会因为我们的断言而消失, 只会在我们最没防备时显形。这给了我一种看待"一切'使用来源不确定之物'之事"时的清醒:每当我要使用一个"从外部接来、自己控制不了其来源/内容"的东西时, 要追问"我是真的知道它的类型/结构/内容, 还是只是想当然地假设?谁决定了它实际是什么?我有没有依据保证它符合我的预期?"——对没有依据保证的, 诚实地当它"未知"、先确认/校验再用, 而不是用一句断言假装自己知道;"对不确定来源诚实地承认未知、先确认再用而非想当然假设", 是写对异常处理、也是稳健对待一切外部输入的关键。认清 throw 可抛任意值、catch 到的是 unknown 是诚实不是死板、用前要 instanceof Error 收窄——这,是我用一次差点丢失错误信息的事故,换来的、关于 TypeScript、也关于如何诚实面对不确定性的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次在 catch 里顺手写 e.message、被 TS 拦住而心生烦躁时,先想想"它真的一定是 Error 吗?TS 是不是在提醒我先确认?",并用 instanceof Error 收窄一下,那我对着那个"Object is of type unknown"的报错纠结的那阵子,就值了。
—— 别看了 · 2026