错误处理与类型
本教程共 80 篇 · 第 77 篇 · 更新于 2026-08-10 · 约 14 分钟阅读
本节目标:把错误处理里的类型问题讲透——为什么 TS 7.0 下
catch的err是unknown、怎么安全地使用它、如何设计自定义 Error 类、以及用Result<T, E>模式把错误从”抛”变成”值”的利与弊。
useUnknownInCatchVariables
这是对 TS 错误处理影响最大的一个配置项。TS 7.0 的 strict: true 默认开启了它:
{
"compilerOptions": {
"strict": true // 自动开启 useUnknownInCatchVariables
}
}
开启后,catch 的变量从 any 变成了 unknown:
try {
throw new Error("出错了");
} catch (err) {
// TS 7.0 strict 下 err 类型:unknown
console.error(err.message); // ❌ 'err' is of type 'unknown'
}
为什么这样做?因为在 JavaScript 里,throw 可以抛出任何值:
try {
throw 42; // 数字
throw "error"; // 字符串
throw undefined; // undefined
throw null; // null
} catch (err) {
// err 可能是任何类型,不能假设它有 .message
}
any 太宽松了,你会不假思索地写 err.message,运行时却抛出一个 “message” is not defined 的错误。unknown 强迫你在使用前做类型收窄。
处理 unknown 错误的正确姿势
标准做法是用 instanceof 检查:
try {
riskyOperation();
} catch (err) {
if (err instanceof Error) {
// 这里 err: Error —— 安全访问 .message、.stack
console.error(err.message);
} else {
// 非标准错误,手动转成字符串
console.error("未知错误:", String(err));
}
}
function getErrorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
return String(err);
}
try {
JSON.parse("{ bad json }");
} catch (err) {
const msg = getErrorMessage(err);
console.error(msg); // "Unexpected token b in JSON at position 2"
}
如果你确定只抛 Error
你也可以用类型断言,但断言过多说明你的错误处理不规范:
try {
doSomething();
} catch (err) {
const error = err as Error; // 断言:我确定只抛 Error
console.error(error.message);
}
更好的做法是编码规范里约定只抛 Error 实例,这样 instanceof Error 就能覆盖所有情况。
关闭 useUnknownInCatchVariables
不推荐,但如果你有大量旧代码且不想改,可以关掉:
{
"compilerOptions": {
"useUnknownInCatchVariables": false
}
}
关掉后 catch (err) 的 err 回到 any,能写 err.message 不报错——但失去了编译期保护。
Error 类的类型体系
回顾一下上一章提到的 Error 类型体系:
// 内置 Error 子类
new Error("generic error"); // Error
new TypeError("type mismatch"); // TypeError extends Error
new RangeError("index out of range"); // RangeError extends Error
new SyntaxError("bad syntax"); // SyntaxError extends Error
new ReferenceError("not defined"); // ReferenceError extends Error
new URIError("bad URI"); // URIError extends Error
new EvalError("eval failed"); // EvalError extends Error
类型判断也按继承链走:
const te = new TypeError("oops");
console.log(te instanceof TypeError); // true
console.log(te instanceof Error); // true(因为 TypeError extends Error)
自定义 Error 子类
业务里经常需要自定义 Error,携带更多上下文:
class ApiError extends Error {
constructor(
message: string,
public statusCode: number,
public responseBody?: unknown
) {
super(message);
this.name = "ApiError";
// TS 7.0 target 为 ES2015+,不需要手动修复原型链
}
}
async function fetchData(url: string): Promise<any> {
const response = await fetch(url);
if (!response.ok) {
const body = await response.text();
throw new ApiError(`请求失败: ${url}`, response.status, body);
}
return response.json();
}
async function main() {
try {
const data = await fetchData("/api/users");
} catch (err) {
if (err instanceof ApiError) {
// err: ApiError —— 能访问 statusCode、responseBody
console.error(`HTTP ${err.statusCode}: ${err.message}`);
if (err.responseBody) {
console.error("响应体:", err.responseBody);
}
} else if (err instanceof Error) {
console.error("其他错误:", err.message);
}
}
}
注意 instanceof 检查的顺序:先检查更具体的 ApiError,再检查 Error。因为 ApiError extends Error,ApiError 实例同时满足两个检查——顺序反了就会进错分支。
TipTS 7.0 默认 target 是 ES2015,而 ES2015 的
class语法会自动正确处理原型链。所以不需要像老教程那样写Object.setPrototypeOf(this, ApiError.prototype)。
自定义 Error 的链式结构
class NetworkError extends Error {
constructor(message: string, public cause?: Error) {
super(message);
this.name = "NetworkError";
}
}
class DatabaseError extends Error {
constructor(message: string, public cause?: Error) {
super(message, { cause }); // ES2022 的 error cause
this.name = "DatabaseError";
}
}
Error 构造函数的第二个参数支持 { cause }(ES2022),可以用来链式记录原始错误。TS 7.0 完全支持此语法。
Result<T, E> 模式简介
try/catch 有个先天不足:错误类型不在函数签名里。你看一个函数声明 function parse(data: string): User,完全不知道它会不会抛错、抛什么错。
Result<T, E> 模式来自 Rust 等函数式语言,把错误从”抛”变成”返回值”:
// Result 的定义
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
// 成功
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
// 失败
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
使用方式:
function parseJSON(text: string): Result<unknown, SyntaxError> {
try {
return ok(JSON.parse(text));
} catch (e) {
if (e instanceof SyntaxError) {
return err(e);
}
throw e; // 非 SyntaxError 继续往外抛
}
}
const result = parseJSON('{"name": "Alice"}');
if (result.ok) {
console.log(result.value); // unknown
} else {
console.error(result.error.message); // SyntaxError
}
Result 的优点
- 错误类型在签名里——一眼看到可能返回什么错误
- 调用方被迫处理——
result.ok检查是编译期强制的 - 不用 try/catch——控制流更线性
Result 的缺点
- 不是 TS/JS 的惯用风格——团队可能不熟悉
- 和 Promise 不兼容——
async/await没法直接和 Result 配合 - 需要额外包装层——每个函数都要 wrap
折中方案:只在关键边界(如数据解析、API 适配层)用 Result,内部维持 throw/catch。
Result 与 async
如果想在 async 函数中用,可以定义 AsyncResult<T, E>:
type AsyncResult<T, E = Error> = Promise<Result<T, E>>;
async function fetchUser(id: number): AsyncResult<User, NetworkError> {
try {
const res = await fetch(`/api/user/${id}`);
if (!res.ok) return err(new NetworkError(`HTTP ${res.status}`));
const data = await res.json();
return ok(data as User);
} catch (e) {
return err(new NetworkError("网络请求失败"));
}
}
async function main() {
const result = await fetchUser(1);
if (result.ok) {
console.log(result.value.name);
} else {
console.error(result.error.message);
}
}
neverthrow 等第三方库
社区已有成熟的 Result 实现,比如 neverthrow:
import { ok, err, Result } from "neverthrow";
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return err("除数不能为 0");
return ok(a / b);
}
const result = divide(10, 2);
result.match(
(value) => console.log("结果:", value),
(error) => console.error("错误:", error)
);
neverthrow 提供了 map、andThen、match、unwrapOr 等函数式操作,比手写 if (result.ok) 更优雅。但对入门教程来说,先理解 Result<T, E> 的核心思想就够了。
小结
- TS 7.0 strict 下
catch (err)中err类型是unknown,使用前必须收窄 - 用
instanceof Error是最标准的收窄方式;也可以统一约定只抛 Error 实例 - 自定义 Error 子类可以携带业务上下文(状态码、字段名、原始错误等)
instanceof检查时先检查子类再检查父类,顺序很重要Result<T, E>把错误从”抛”变成”返回值”,让错误类型出现在函数签名里- Result 在 TS 生态中不惯用,但适合在关键边界处引入
- 成熟的 Result 库如
neverthrow提供了函数式 API,可按需选用