目录
JavaScript 8. 错误处理与异常流控制
错误处理的目的不是让程序“永远不出错”,而是让不同类型的失败能够被准确识别、在合适的层级处理,并在无法恢复时保留足够的信息继续向上传递。
JavaScript 中的异常是一种控制流机制。执行 throw,或运行时检测到某些非法操作时,当前正常执行会中断,控制权沿调用关系向外查找最近的匹配处理位置。如果没有任何代码处理该异常,最终行为由宿主环境决定,例如浏览器可能报告未捕获错误,Node.js 进程可能根据错误类型和运行配置终止。
需要区分以下概念:
- 程序错误:代码违反了语言规则或接口契约,例如调用不可调用值;
- 输入错误:外部数据不符合预期格式;
- 业务失败:操作在技术上正常完成,但业务条件不允许继续;
- 环境失败:网络、磁盘、权限或外部服务不可用;
- 取消:操作因为用户行为、超时或上层决策而被主动终止;
- 异常对象:用于描述失败原因的 JavaScript 值;
- 异常控制流:通过
throw、try、catch和finally改变执行路径。
错误处理不应把所有失败都压缩成一个布尔值,也不应在每一层重复打印后继续抛出同一个错误。一个可靠的设计通常需要明确:
- 哪些错误可以在当前层恢复;
- 哪些错误应转换为更高层语义;
- 哪些错误必须继续抛出;
- 哪些信息可以展示给用户;
- 哪些信息只应记录在日志或监控系统中;
- 资源如何在成功、失败和取消时统一清理。
8.1 并非所有异常结果都会抛出错误
JavaScript 的一些运算会返回特殊值,而不是抛出异常。
console.log(10 / 0); // Infinity
console.log(0 / 0); // NaN
数组越界读取返回 undefined:
const values = [
1,
2,
3,
];
console.log(values[10]); // undefined
读取不存在的普通对象属性也返回 undefined:
const user = {
name: "Alice",
};
console.log(user.age); // undefined
因此,不能假设所有不合理结果都会自动进入 catch。程序仍需要主动验证:
function divide(
dividend,
divisor,
) {
if (divisor === 0) {
throw new RangeError(
"divisor 不能为 0",
);
}
return dividend / divisor;
}
是否应该抛出异常取决于函数契约。如果某个结果在领域中具有明确意义,也可以返回结构化结果:
function divideSafely(
dividend,
divisor,
) {
if (divisor === 0) {
return {
ok: false,
reason: "division-by-zero",
};
}
return {
ok: true,
value: dividend / divisor,
};
}
异常适合表达无法在当前正常返回通道中继续处理的失败;预期频繁出现的普通分支不一定都需要异常。
8.2 throw 语句
8.2.1 抛出异常
throw new Error(
"Operation failed",
);
执行 throw 后,当前代码路径立即中断:
function run() {
console.log("before");
throw new Error("failed");
// 不会执行。
console.log("after");
}
8.2.2 可以抛出任意值
JavaScript 允许抛出任意值:
throw "failed";
throw 404;
throw null;
throw {
reason: "invalid-data",
};
但工程代码应优先抛出 Error 或其子类:
throw new TypeError(
"options 必须是对象",
);
原因包括:
- 具有统一的
name和message; - 通常具有调用栈信息;
- 可以通过错误类型分类;
- 支持
cause; - 更容易被日志和监控工具识别;
- 可以添加结构化业务字段。
外部库和旧代码仍可能抛出字符串、对象或其他值,因此 catch 中不应无条件假定捕获值一定是 Error。
8.2.3 throw 后不能换行
throw 属于受限产生式,关键字和表达式之间不能出现换行:
// SyntaxError
// throw
// new Error("failed");
正确写法:
throw new Error(
"failed",
);
表达式较长时,可以在同一行开始圆括号:
throw (
new Error(
"failed",
)
);
这与 return 的 ASI 陷阱相关,但 throw 的换行会直接产生语法错误,而不是静默抛出 undefined。
8.2.4 重新抛出
try {
performOperation();
} catch (error) {
if (
error instanceof ExpectedError
) {
recover(error);
} else {
throw error;
}
}
throw error 会继续抛出原错误对象,保留其身份和已有信息。
不要写成:
throw new Error(
error.message,
);
这种做法会创建新错误并丢失原错误类型、结构化字段和原始错误链。确实需要转换错误语义时,应使用 cause:
throw new ApplicationError(
"无法完成用户数据加载",
{
cause: error,
},
);
8.3 try…catch
8.3.1 基本语法
try {
const data = JSON.parse(
sourceText,
);
console.log(data);
} catch (error) {
console.error(
"解析失败:",
error,
);
}
try 块中的代码正常完成时,catch 不执行。发生抛出完成时,控制流转入 catch。
8.3.2 catch 参数的作用域
try {
throw new Error("failed");
} catch (error) {
console.log(error.message);
}
// ReferenceError
// console.log(error);
catch 参数只在对应 catch 块中可见。
8.3.3 可选 catch 绑定
不需要错误值时,可以省略参数:
try {
JSON.parse(sourceText);
} catch {
return defaultValue;
}
适合以下情况:
- 错误细节确实不影响处理;
- 所有失败都采用同一明确降级策略;
- 错误已经在更底层被记录;
- 只是检测某项操作是否可行。
不要为了简短而省略有助于诊断的信息:
try {
await saveData();
} catch {
showMessage(
"保存失败",
);
}
如果没有任何日志、重试或上报,真实故障原因可能完全丢失。
8.3.4 catch 可以捕获任意抛出值
try {
throw "failed";
} catch (error) {
console.log(
typeof error,
); // string
}
需要安全读取信息时,可以先标准化:
function toError(value) {
if (
typeof Error.isError === "function"
&& Error.isError(value)
) {
return value;
}
if (value instanceof Error) {
return value;
}
if (
typeof value === "string"
) {
return new Error(value);
}
try {
return new Error(
`Non-Error thrown: ${
JSON.stringify(value)
}`,
{
cause: value,
},
);
} catch {
return new Error(
"A non-Error value was thrown",
{
cause: value,
},
);
}
}
这个辅助函数只用于建立统一日志入口。它不会改变原始抛出行为,是否需要包装仍取决于当前错误边界。
8.4 try…catch 的同步边界
8.4.1 捕获同步调用链中的异常
function inner() {
throw new Error("failed");
}
function middle() {
inner();
}
try {
middle();
} catch (error) {
console.error(error.message);
}
异常沿当前同步调用关系向外传播,因此外层可以捕获。
8.4.2 无法捕获未来任务中的异常
try {
setTimeout(() => {
throw new Error(
"timer failed",
);
}, 0);
} catch (error) {
// 不会执行。
console.error(error);
}
外层 try 在定时器回调执行前已经正常结束。未来任务拥有新的调用关系,不处于原 try 的动态执行范围内。
应该在回调内部处理:
setTimeout(() => {
try {
performTimerWork();
} catch (error) {
reportError(error);
}
}, 0);
或者把回调式 API 转换为 Promise,并在 Promise 链中处理。
8.4.3 Promise 拒绝不是同步 throw
try {
Promise.reject(
new Error("failed"),
);
} catch (error) {
// 不会执行。
}
创建被拒绝的 Promise 不会在当前调用点同步抛出相同异常。应处理返回的 Promise:
Promise.reject(
new Error("failed"),
).catch((error) => {
console.error(error);
});
或者在 async 函数中等待:
try {
await Promise.reject(
new Error("failed"),
);
} catch (error) {
console.error(error);
}
await 会在当前异步函数恢复位置把拒绝转换为抛出完成,因此可以由该 try...catch 捕获。
8.4.4 Promise 执行器中的同步异常
const promise = new Promise(() => {
throw new Error(
"executor failed",
);
});
Promise 构造器会把执行器同步抛出的异常转换为 Promise 拒绝:
promise.catch((error) => {
console.error(error.message);
});
但执行器启动的未来回调仍需显式 reject():
const promise = new Promise(
(
resolve,
reject,
) => {
setTimeout(() => {
try {
performWork();
resolve("completed");
} catch (error) {
reject(error);
}
}, 0);
},
);
8.5 finally
8.5.1 基本语义
finally 会在 try 或 catch 完成后执行:
try {
openResource();
useResource();
} catch (error) {
handleError(error);
} finally {
closeResource();
}
无论正常完成还是异常完成,清理代码都有机会执行。
try 可以与 finally 直接组合:
try {
useResource();
} finally {
closeResource();
}
8.5.2 finally 在 return 前执行
function example() {
try {
return "result";
} finally {
console.log("cleanup");
}
}
console.log(example());
输出:
cleanup
result
返回值已经确定,但函数真正退出前先执行 finally。
8.5.3 finally 在 throw 继续传播前执行
function example() {
try {
throw new Error("failed");
} finally {
console.log("cleanup");
}
}
try {
example();
} catch (error) {
console.error(error.message);
}
finally 完成后,原异常继续传播。
8.5.4 finally 中的控制流会覆盖原结果
function example() {
try {
return "from try";
} finally {
return "from finally";
}
}
console.log(
example(),
); // from finally
finally 中的 return 覆盖原返回值。
更严重的是覆盖异常:
function example() {
try {
throw new Error(
"original failure",
);
} finally {
return "success";
}
}
console.log(
example(),
); // success
原异常完全消失。
同理,finally 中重新抛出错误会覆盖原来的返回或异常:
function example() {
try {
throw new Error(
"original failure",
);
} finally {
throw new Error(
"cleanup failure",
);
}
}
调用者只能直接收到清理错误。
因此,finally 通常只做清理,不应包含:
return;break;continue;- 与清理无关的业务分支;
- 容易失败且未受控制的复杂操作。
8.5.5 清理本身可能失败
let originalError;
try {
performOperation();
} catch (error) {
originalError = error;
throw error;
} finally {
closeResource();
}
如果 closeResource() 抛出错误,原错误会被新的清理错误覆盖。
可以显式保留两者:
let operationError;
try {
performOperation();
} catch (error) {
operationError = error;
throw error;
} finally {
try {
closeResource();
} catch (cleanupError) {
if (
operationError !== undefined
) {
throw new AggregateError(
[
operationError,
cleanupError,
],
"操作和资源清理均失败",
);
}
throw cleanupError;
}
}
这种模式较复杂。更常见的做法是让资源接口的关闭操作尽量具有幂等性,并使用明确的资源管理抽象。
8.6 Error 对象
8.6.1 创建 Error
const error = new Error(
"Operation failed",
);
Error() 与 new Error() 在标准行为上等价:
const first = Error("failed");
const second = new Error(
"failed",
);
为了明确表示创建对象,工程代码通常使用 new Error()。
8.6.2 message 与 name
const error = new Error(
"Operation failed",
);
console.log(error.name);
// Error
console.log(error.message);
// Operation failed
message 通常是实例的不可枚举自有属性。name 默认从原型继承。
console.log(
Object.keys(error),
); // []
因此:
JSON.stringify(error);
通常得到:
{}
需要序列化错误时,应显式提取字段。
8.6.3 Error.prototype.toString
console.log(
String(
new TypeError(
"Invalid value",
),
),
);
// TypeError: Invalid value
基本组合规则为:
name为空时返回message;message为空时返回name;- 两者都存在时返回
"name: message"。
8.6.4 stack
现代运行环境通常提供:
error.stack
它一般包含错误名称、信息和调用栈,但 stack 尚不是 ECMAScript 2026 的标准属性。其格式、生成时机、可写性、是否包含异步调用关系以及最大深度都取决于运行环境。
因此:
- 可以用于调试和日志;
- 不应按固定字符串格式解析业务数据;
- 不应把某一引擎的堆栈格式作为跨环境协议;
- 不应把堆栈直接展示给最终用户;
- 堆栈可能包含文件路径、源代码位置或其他敏感信息。
8.6.5 Error.captureStackTrace
V8 系列环境通常提供:
Error.captureStackTrace(
targetObject,
constructorFunction,
);
它可以在自定义错误中控制堆栈起点:
class ApplicationError
extends Error {
constructor(
message,
options,
) {
super(
message,
options,
);
this.name = (
this.constructor.name
);
if (
typeof Error.captureStackTrace
=== "function"
) {
Error.captureStackTrace(
this,
this.constructor,
);
}
}
}
这不是 ECMAScript 标准接口。使用前必须进行功能检测,且不能依赖其输出格式。
8.7 Error.cause
8.7.1 基本用法
标准错误构造器支持 options.cause:
const lowLevelError
= new Error(
"Connection refused",
);
const highLevelError
= new Error(
"Unable to load user data",
{
cause: lowLevelError,
},
);
console.log(
highLevelError.cause
=== lowLevelError,
); // true
cause 是不可枚举自有属性,因此不会出现在 Object.keys() 中。
8.7.2 错误包装
async function loadConfiguration() {
try {
const response = await fetch(
"/config.json",
);
if (!response.ok) {
throw new Error(
`HTTP ${response.status}`,
);
}
return await response.json();
} catch (error) {
throw new ConfigurationError(
"无法加载配置",
{
cause: error,
},
);
}
}
包装后,上层可以处理统一的业务错误,同时保留底层原因。
8.7.3 cause 可以是任意值
const error = new Error(
"failed",
{
cause: {
code: "E_TIMEOUT",
},
},
);
标准没有要求 cause 必须是 Error。工程上仍建议优先保存 Error 或结构明确的原因对象。
8.7.4 不要把 cause 拼进 message
不推荐:
throw new Error(
`Configuration failed: ${
error.message
}`,
);
问题包括:
- 机器难以区分高层和底层信息;
- 丢失底层类型;
- 丢失结构化字段;
- 多层包装会形成冗长重复文本;
- 不利于日志系统遍历原因链。
推荐:
throw new ConfigurationError(
"无法加载配置",
{
cause: error,
},
);
用户界面只展示高层信息,日志系统可以记录完整原因链。
8.8 Error.isError
ECMAScript 2026 引入 Error.isError(),用于判断对象是否带有规范定义的 [[ErrorData]] 内部槽。它可识别 Error、各原生错误类型、AggregateError,以及继承这些类型的自定义错误实例。
console.log(
Error.isError(
new Error("failed"),
),
); // true
console.log(
Error.isError(
new TypeError("failed"),
),
); // true
console.log(
Error.isError(
new AggregateError(
[],
"failed",
),
),
); // true
普通伪装对象不会通过:
const fakeError = {
name: "Error",
message: "failed",
[Symbol.toStringTag]: "Error",
};
console.log(
Error.isError(fakeError),
); // false
Error.isError() 检查标准内部错误标记,而不是只看原型或属性名称。
8.8.1 与 instanceof 的区别
error instanceof Error
依赖当前 Realm 的 Error.prototype 是否出现在对象原型链中。来自 iframe 等其他 Realm 的 Error 可能无法通过当前 Realm 的 instanceof Error。
Error.isError() 根据标准内部槽判断,适合识别跨 Realm 的标准错误对象。
8.8.2 兼容性
Error.isError() 属于 ECMAScript 2026 新接口,旧运行环境可能尚未实现。
可以提供兼容路径:
function isError(value) {
if (
typeof Error.isError
=== "function"
) {
return Error.isError(value);
}
return value instanceof Error;
}
后备的 instanceof 不能完整解决跨 Realm 问题,也不能完全模拟标准内部槽判断。它只是旧环境中的近似方案。
8.9 内置错误类型
JavaScript 定义了 Error 以及若干原生错误类型。
| 类型 | 典型含义 |
|---|---|
Error | 通用错误 |
TypeError | 操作无法用于当前类型或对象状态 |
RangeError | 数值或参数超出允许范围 |
ReferenceError | 无法解析或访问有效引用 |
SyntaxError | 解析的代码或特定语法文本不合法 |
URIError | URI 编码或解码输入不合法 |
EvalError | 历史兼容类型,现代规范不主动使用 |
AggregateError | 一组错误的聚合 |
8.9.1 TypeError
典型情况:
const value = null;
// TypeError
// value.name;
const value = 10;
// TypeError
// value();
Object.defineProperty(
Object.freeze({}),
"value",
{
value: 10,
},
);
// TypeError
TypeError 不只表示“形参类型错误”。当一个操作在当前对象状态下无法完成,而没有更具体原生错误类型时,也常使用 TypeError。
8.9.2 RangeError
// RangeError
// new Array(-1);
// RangeError
// (1).toFixed(101);
递归过深导致的调用栈错误,在不同引擎中通常表现为 RangeError,但错误信息和具体阈值由实现决定。
业务参数超出允许范围时,也可以主动抛出 RangeError:
function setOpacity(value) {
if (
typeof value !== "number"
) {
throw new TypeError(
"opacity 必须是数值",
);
}
if (
value < 0
|| value > 1
) {
throw new RangeError(
"opacity 必须位于 0 到 1",
);
}
}
8.9.3 ReferenceError
// ReferenceError
// console.log(
// missingVariable,
// );
暂时性死区访问也会产生 ReferenceError:
{
// ReferenceError
// console.log(value);
const value = 10;
}
对象不存在某个属性不会产生 ReferenceError,只会返回 undefined:
console.log(
({
name: "Alice",
}).missing,
); // undefined
8.9.4 SyntaxError
源文件本身存在语法错误时,整个脚本或模块可能在执行前就无法解析:
// 以下代码不能与当前文件共存并被正常执行。
// const = 10;
同一源文件中的 try...catch 无法捕获使该文件整体解析失败的静态语法错误,因为程序尚未开始执行。
但某些运行时解析操作会抛出可捕获的 SyntaxError:
try {
JSON.parse(
"{ invalid json }",
);
} catch (error) {
console.log(
error instanceof SyntaxError,
); // true
}
try {
new Function(
"const = 10;",
);
} catch (error) {
console.log(
error instanceof SyntaxError,
); // true
}
动态导入的模块解析失败会通过返回 Promise 的拒绝通道传播,而不是由外围同步 try 直接捕获。
8.9.5 URIError
try {
decodeURIComponent("%");
} catch (error) {
console.log(
error instanceof URIError,
); // true
}
URIError 主要与:
encodeURI();decodeURI();encodeURIComponent();decodeURIComponent()。
等全局 URI 处理函数有关。
8.9.6 EvalError
const error = new EvalError(
"Legacy error type",
);
现代 ECMAScript 规范不使用 EvalError 表示 eval() 失败。它保留用于兼容旧版本和既有代码。
不要因为使用 eval() 就预期捕获 EvalError。实际错误通常是 SyntaxError、ReferenceError、TypeError 或被执行代码主动抛出的其他值。
8.10 AggregateError
8.10.1 创建聚合错误
const error = new AggregateError(
[
new Error("First failure"),
new TypeError(
"Second failure",
),
],
"Multiple operations failed",
);
错误集合保存在 errors 属性中:
console.log(error.errors);
构造器接收可迭代对象,并把其中值收集为数组。元素不必是 Error,但工程上通常应使用 Error 对象。
errors 是可写、可配置但不可枚举的自有属性。
8.10.2 Promise.any
Promise.any() 在所有输入 Promise 都拒绝时,以 AggregateError 拒绝:
try {
await Promise.any([
Promise.reject(
new Error("source A failed"),
),
Promise.reject(
new Error("source B failed"),
),
]);
} catch (error) {
if (
error instanceof AggregateError
) {
for (
const cause
of error.errors
) {
console.error(cause);
}
}
}
8.10.3 批量操作
async function saveAll(items) {
const results
= await Promise.allSettled(
items.map(
item => saveItem(item),
),
);
const errors = [];
for (
const result
of results
) {
if (
result.status
=== "rejected"
) {
errors.push(
toError(result.reason),
);
}
}
if (
errors.length > 0
) {
throw new AggregateError(
errors,
`共有 ${errors.length} 项保存失败`,
);
}
}
AggregateError 适合多个相互独立的失败同时需要保留,而不是只记录第一个错误。
8.10.4 聚合不等于简单字符串拼接
不推荐:
throw new Error(
errors
.map(
error => error.message,
)
.join("; "),
);
这样会丢失:
- 每个错误的类型;
- 调用栈;
cause;- 自定义字段;
- 错误对象身份。
AggregateError 保留了结构化错误集合。
8.11 自定义错误类
8.11.1 基本定义
class ApplicationError
extends Error {
constructor(
message,
options = {},
) {
super(
message,
options,
);
this.name = (
this.constructor.name
);
}
}
派生错误:
class ValidationError
extends ApplicationError {}
class NetworkError
extends ApplicationError {}
class ConfigurationError
extends ApplicationError {}
8.11.2 结构化字段
class ValidationError
extends ApplicationError {
constructor(
message,
{
field,
value,
cause,
} = {},
) {
super(
message,
{
cause,
},
);
this.field = field;
this.value = value;
}
}
使用:
throw new ValidationError(
"email 格式无效",
{
field: "email",
value: input.email,
},
);
调用者可以根据类型和字段处理,而不需要解析 message:
try {
validateInput(input);
} catch (error) {
if (
error
instanceof ValidationError
) {
highlightField(
error.field,
);
} else {
throw error;
}
}
8.11.3 错误代码
公共库或跨进程接口有时需要稳定错误代码:
class ApplicationError
extends Error {
constructor(
message,
{
code,
cause,
details,
} = {},
) {
super(
message,
{
cause,
},
);
this.name = (
this.constructor.name
);
this.code = code;
this.details = details;
}
}
throw new ApplicationError(
"用户不存在",
{
code: "USER_NOT_FOUND",
details: {
userId,
},
},
);
错误代码应:
- 稳定;
- 可枚举文档化;
- 不依赖本地化文本;
- 不包含敏感信息;
- 与 HTTP 状态等外部协议概念保持适当解耦。
不要根据 message 文本分支:
// 不可靠
if (
error.message
=== "User not found"
) {
// ...
}
文本可能因为翻译、措辞修改或运行环境而变化。
8.11.4 name 的稳定性
this.name = (
this.constructor.name
);
适合调试,但类名可能被构建工具压缩或重命名。
如果错误名称必须成为稳定协议,应显式赋值:
class ValidationError
extends Error {
constructor(
message,
options,
) {
super(
message,
options,
);
this.name
= "ValidationError";
}
}
业务分支仍应优先使用类身份或稳定 code,而不是只使用 name 字符串。
8.11.5 继承 Error 的注意事项
现代原生类继承通常能够正确建立 Error 原型关系:
const error
= new ValidationError(
"invalid",
);
console.log(
error instanceof Error,
); // true
console.log(
error
instanceof ValidationError,
); // true
如果代码被转换到非常旧的 JavaScript 运行环境,构建工具对内置类继承的转换可能需要额外支持。应通过目标环境测试,而不是依赖现代环境中的结果推断所有旧环境。
8.12 错误分类与处理层级
8.12.1 可恢复错误
例如:
- 用户输入不合法;
- 可选配置缺失;
- 请求可以重试;
- 非关键功能不可用;
- 缓存读取失败但可重新计算。
try {
return parseUserInput(input);
} catch (error) {
if (
error
instanceof ValidationError
) {
return {
ok: false,
issues: error.details,
};
}
throw error;
}
8.12.2 不可恢复错误
例如:
- 程序内部不变量被破坏;
- 必需服务持续不可用;
- 数据状态不一致;
- 未知编程错误;
- 安全验证失败。
当前层无法恢复时,应:
- 保留错误信息;
- 执行必要清理;
- 继续抛出;
- 由更高层决定是否终止操作或应用。
8.12.3 预期失败与程序缺陷
const result = await findUser(
userId,
);
“用户不存在”可能是预期业务结果,可以返回:
{
found: false,
}
也可以在某些接口中抛出 UserNotFoundError。关键是保持契约一致。
而访问未定义变量、把字符串当函数调用等通常属于程序缺陷,不应被无条件转换为空结果继续运行。
8.12.4 取消不一定等同于失败
用户主动取消上传,通常不应和服务器崩溃采用相同提示和监控级别。
try {
await uploadFile(
file,
{
signal,
},
);
} catch (error) {
if (
signal.aborted
) {
return {
status: "cancelled",
reason: signal.reason,
};
}
throw error;
}
具体 API 可能抛出 DOMException,其名称可能为 "AbortError"。与其只比较错误文本,更可靠的做法通常是同时检查当前信号状态和 API 文档规定的错误类型。
8.13 重新抛出的原则
8.13.1 只处理当前层能够解决的错误
function parseConfiguration(
sourceText,
) {
try {
return JSON.parse(
sourceText,
);
} catch (error) {
if (
error instanceof SyntaxError
) {
throw new ConfigurationError(
"配置文件不是合法 JSON",
{
cause: error,
},
);
}
throw error;
}
}
当前函数负责配置解析,因此可以把 JSON SyntaxError 转换为 ConfigurationError。
如果出现意外 TypeError,不应一并吞掉。
8.13.2 避免 catch 过宽
不推荐:
try {
const data = JSON.parse(
sourceText,
);
return data.profile.id;
} catch {
return null;
}
这里同时吞掉:
- JSON 语法错误;
profile缺失导致的 TypeError;- getter 抛出的错误;
- 未来新增逻辑中的错误。
应缩小捕获范围:
let data;
try {
data = JSON.parse(
sourceText,
);
} catch (error) {
throw new ConfigurationError(
"配置 JSON 无法解析",
{
cause: error,
},
);
}
if (
data === null
|| typeof data !== "object"
|| data.profile === null
|| typeof data.profile
!== "object"
) {
throw new ValidationError(
"配置缺少 profile 对象",
);
}
return data.profile.id;
8.13.3 记录后重新抛出可能造成重复日志
try {
await operation();
} catch (error) {
console.error(error);
throw error;
}
如果每一层都这样写,同一错误会被记录多次。
更好的策略通常是:
- 底层添加结构化上下文或
cause; - 中间层只在能够恢复时处理;
- 顶层错误边界统一记录;
- 必要时通过标记防止重复上报。
8.14 Promise 链中的错误传播
8.14.1 处理器抛出会产生拒绝
Promise.resolve(
sourceText,
)
.then((text) => {
return JSON.parse(text);
})
.then((data) => {
return processData(data);
})
.catch((error) => {
console.error(error);
});
JSON.parse() 抛出的错误会使当前 .then() 返回的新 Promise 被拒绝,并沿链传播。
8.14.2 catch 返回值会恢复链
Promise.reject(
new Error("failed"),
)
.catch((error) => {
console.error(error);
return [];
})
.then((values) => {
console.log(values); // []
});
catch 正常返回后,后续链恢复为 fulfilled。
如果当前层无法恢复,应继续抛出:
.catch((error) => {
if (
error
instanceof OptionalDataError
) {
return [];
}
throw error;
});
8.14.3 同一个 then 的第二参数边界
promise.then(
() => {
throw new Error(
"onFulfilled failed",
);
},
(error) => {
// 不能捕获同一次 then
// 的 onFulfilled 抛出的错误。
},
);
该拒绝处理器只处理上游 Promise 的拒绝。
后接 .catch() 可以捕获兑现处理器抛出的错误:
promise
.then(() => {
throw new Error(
"onFulfilled failed",
);
})
.catch((error) => {
console.error(error);
});
8.14.4 finally 失败会覆盖结果
Promise.resolve(
"original",
)
.finally(() => {
throw new Error(
"cleanup failed",
);
})
.catch((error) => {
console.error(
error.message,
);
});
输出为清理错误。Promise finally() 与语句 finally 都需要谨慎处理清理失败。
8.15 async/await 中的错误处理
8.15.1 基本模式
async function loadUser(
userId,
) {
try {
const response = await fetch(
`/api/users/${userId}`,
);
if (!response.ok) {
throw new NetworkError(
`HTTP ${response.status}`,
{
code: "HTTP_ERROR",
details: {
status: response.status,
},
},
);
}
return await response.json();
} catch (error) {
throw new UserLoadError(
"无法加载用户数据",
{
cause: error,
},
);
}
}
8.15.2 不应捕获后立即返回 undefined
async function loadData() {
try {
return await fetchData();
} catch (error) {
console.error(error);
}
}
失败时函数会兑现为 undefined,调用者可能误以为操作成功但没有数据。
除非接口明确把 undefined 定义为降级结果,否则应:
- 返回明确的后备值;
- 返回结构化 Result;
- 或继续抛出。
async function loadOptionalData() {
try {
return await fetchData();
} catch (error) {
if (
error
instanceof NotFoundError
) {
return null;
}
throw error;
}
}
8.15.3 return await 的错误边界
async function wrapper() {
try {
return fetchData();
} catch (error) {
// 不能捕获 fetchData()
// 返回 Promise 的未来拒绝。
}
}
这里 try 只覆盖调用 fetchData() 的同步部分。
为了在当前 catch 中处理返回 Promise 的拒绝,需要:
async function wrapper() {
try {
return await fetchData();
} catch (error) {
throw new DataLoadError(
"加载失败",
{
cause: error,
},
);
}
}
return await 并非一律冗余。需要当前函数的 try...catch...finally 覆盖拒绝时,它具有明确语义。
8.15.4 未等待的异步操作
async function run() {
try {
saveData();
} catch (error) {
// 无法捕获 saveData()
// 返回 Promise 的未来拒绝。
}
}
应等待:
await saveData();
或者明确处理:
void saveData().catch(
reportError,
);
“即发即弃”任务仍需要错误处理策略和生命周期管理。
8.16 未处理的 Promise 拒绝
8.16.1 浏览器事件
浏览器可以在全局对象上报告未处理拒绝:
globalThis.addEventListener(
"unhandledrejection",
(event) => {
console.error(
"Unhandled rejection:",
event.reason,
);
},
);
如果之后补上处理器,还可能出现 rejectionhandled 事件。
这些事件属于宿主环境接口,不是 ECMAScript Error 对象本身的一部分。
8.16.2 全局监听不是普通控制流
全局监听适合:
- 最后一道遥测;
- 记录意外遗漏;
- 显示通用故障界面;
- 防止错误完全无迹可寻。
它不应替代局部错误处理:
// 不推荐依靠全局事件处理
// 每个请求的正常失败。
void loadUser();
局部代码更了解:
- 当前操作;
- 是否能重试;
- 是否允许降级;
- 用户应看到什么;
- 需要清理哪些资源。
8.16.3 Node.js 行为不同
Node.js 提供 unhandledRejection 和 uncaughtException 等进程事件,但默认策略和退出行为可能随版本和启动参数变化。
不要把浏览器全局事件和 Node.js 进程错误机制写成一套统一规则。服务端程序还需要考虑:
- 进程是否处于不可信状态;
- 是否应停止接收新请求;
- 是否需要优雅关闭;
- 日志是否已经持久化;
- 进程管理器是否负责重启。
8.17 浏览器全局错误
8.17.1 error 事件
globalThis.addEventListener(
"error",
(event) => {
console.error(
event.error
?? event.message,
);
},
);
浏览器的 error 事件可以报告部分未捕获脚本错误和资源加载错误,但事件对象形式会因目标和错误类型不同而变化。
8.17.2 window.onerror
浏览器传统接口:
globalThis.onerror = (
message,
source,
line,
column,
error,
) => {
console.error(
message,
error,
);
};
它属于 Web 平台接口,不是 ECMAScript 标准。
8.17.3 跨源限制
跨源脚本错误如果缺少适当 CORS 配置,浏览器可能只提供有限的 "Script error." 信息,以避免泄露跨源细节。
生产环境的错误监控需要同时配置:
- 脚本的
crossorigin属性; - 服务端 CORS 响应头;
- source map 上传与访问控制;
- 版本号和发布标识;
- 隐私过滤。
8.18 资源清理
8.18.1 finally 清理
const connection
= await openConnection();
try {
await useConnection(
connection,
);
} finally {
await connection.close();
}
在 async 函数中,finally 可以使用 await。
8.18.2 获取资源也可能失败
let connection;
try {
connection
= await openConnection();
await useConnection(
connection,
);
} finally {
if (
connection !== undefined
) {
await connection.close();
}
}
只有资源成功创建后才执行关闭。
8.18.3 多个资源按反向顺序释放
const database
= await openDatabase();
let transaction;
try {
transaction
= await database
.beginTransaction();
await performWork(
transaction,
);
await transaction.commit();
} catch (error) {
if (
transaction !== undefined
) {
await transaction.rollback();
}
throw error;
} finally {
await database.close();
}
后获得的资源通常先释放,因为它可能依赖先获得的资源。
8.18.4 清理应尽量幂等
await connection.close();
await connection.close();
如果接口允许多次关闭而不产生额外副作用,错误路径会更容易管理。
不能控制第三方接口时,应在本地记录状态:
let closed = false;
async function closeOnce() {
if (closed) {
return;
}
closed = true;
await connection.close();
}
仍需考虑第一次关闭失败后是否允许重试,这取决于资源契约。
8.19 重试
8.19.1 不是所有错误都适合重试
适合重试:
- 临时网络中断;
- 服务器暂时过载;
- 限流后等待;
- 可重放的读取请求;
- 明确标记为暂时失败的操作。
不适合自动重试:
- 输入验证失败;
- 权限不足;
- 资源不存在;
- 程序逻辑错误;
- 非幂等写入且没有幂等键;
- 已经明确取消的操作。
8.19.2 基本重试函数
async function retry(
operation,
{
attempts = 3,
delay = 500,
shouldRetry = () => true,
} = {},
) {
let lastError;
for (
let attempt = 1;
attempt <= attempts;
attempt += 1
) {
try {
return await operation(
attempt,
);
} catch (error) {
lastError = error;
const canRetry = (
attempt < attempts
&& shouldRetry(error)
);
if (!canRetry) {
throw error;
}
await new Promise(
(resolve) => {
setTimeout(
resolve,
delay,
);
},
);
}
}
throw lastError;
}
8.19.3 指数退避与抖动
多个客户端同时失败后,如果都按固定时间重试,可能形成同步冲击。
function calculateDelay(
attempt,
baseDelay = 500,
) {
const exponential = (
baseDelay
* 2 ** (attempt - 1)
);
const jitter = (
Math.random()
* baseDelay
);
return exponential + jitter;
}
实际策略应考虑:
- 服务端
Retry-After; - 最大等待时间;
- 总超时;
- 用户取消;
- 操作幂等性;
- 并发限制;
- 业务优先级。
8.19.4 保留最终 cause
try {
return await retry(
() => fetchData(),
{
attempts: 3,
shouldRetry: isTemporaryError,
},
);
} catch (error) {
throw new DataLoadError(
"多次重试后仍无法加载数据",
{
cause: error,
},
);
}
不要把每次失败都无条件包装成新错误,否则原因链可能变得过长。可以在重试器内部保存错误列表,并在最终失败时使用 AggregateError。
8.20 超时与取消
8.20.1 Promise.race 只决定结果,不取消操作
const result
= await Promise.race([
fetchData(),
timeoutPromise(5000),
]);
超时 Promise 获胜后,fetchData() 仍可能继续运行。
需要真实取消时,应使用底层 API 支持的取消机制,例如 AbortController:
async function fetchWithTimeout(
url,
timeout,
) {
const controller
= new AbortController();
const timerId = setTimeout(
() => {
controller.abort(
new Error(
`请求超过 ${timeout} ms`,
),
);
},
timeout,
);
try {
return await fetch(
url,
{
signal: controller.signal,
},
);
} finally {
clearTimeout(timerId);
}
}
8.20.2 区分超时、取消和网络失败
try {
await fetchWithTimeout(
url,
5000,
);
} catch (error) {
if (
signal.aborted
) {
handleCancellation(
signal.reason,
);
return;
}
if (
isTimeoutError(error)
) {
showRetryOption();
return;
}
throw error;
}
不同 API 对中止原因和错误类型的表现可能不同,应依据接口文档设计,而不是仅比较某个浏览器的错误文本。
8.21 Result 模式
异常不是所有失败表示的唯一选择。对于频繁、预期且调用者必须分支处理的失败,可以返回 Result 对象。
function parseNumber(text) {
const value = Number(text);
if (
Number.isNaN(value)
) {
return {
ok: false,
error: new ValidationError(
"输入不是有效数值",
),
};
}
return {
ok: true,
value,
};
}
使用:
const result = parseNumber(
input,
);
if (result.ok) {
console.log(result.value);
} else {
console.error(
result.error.message,
);
}
8.21.1 适用场景
Result 适合:
- 失败是正常业务分支;
- 调用者必须显式处理;
- 不希望通过异常跳过多层控制流;
- 需要函数式组合;
- 接口跨越 Worker 或序列化边界;
- 错误需要作为普通数据保存。
异常适合:
- 当前操作无法正常继续;
- 错误应沿调用链传播;
- 大多数调用者无法就地恢复;
- 语言和库 API 已经使用异常契约;
- 需要保留调用栈。
8.21.2 不要混乱使用
同一函数不应有时返回:
{
ok: false,
}
有时又对同一类失败抛出异常,除非接口文档明确区分两种通道。
Result 也不意味着永远不抛出异常。程序缺陷、运行时错误和无法建立 Result 的故障仍可能抛出。
8.22 错误日志
8.22.1 记录上下文
仅记录:
console.error(
error.message,
);
通常不够。
更有价值的上下文包括:
- 操作名称;
- 请求或任务标识;
- 用户会话的非敏感标识;
- 应用版本;
- 环境;
- 相关参数摘要;
- 错误类型;
code;cause链;- 调用栈;
- 是否重试;
- 是否取消。
reportError(
error,
{
operation: "load-user",
userId,
release: APP_VERSION,
},
);
8.22.2 不记录敏感数据
避免记录:
- 密码;
- 访问令牌;
- Cookie;
- 完整身份证件;
- 银行信息;
- 医疗隐私;
- 私有消息;
- 原始请求体中的敏感字段;
- 不必要的精确位置。
日志系统本身也是数据系统,需要:
- 访问控制;
- 保留期限;
- 脱敏;
- 加密;
- 合规管理。
8.22.3 结构化日志
console.error({
level: "error",
event: "user-load-failed",
error: {
name: error.name,
message: error.message,
code: error.code,
},
context: {
userId,
},
});
实际日志平台通常会提供专门 API。不要依赖 console 对对象的显示格式作为长期协议。
8.22.4 序列化原因链
function serializeError(
error,
seen = new Set(),
) {
if (
!isError(error)
) {
return {
value: String(error),
};
}
if (seen.has(error)) {
return {
name: error.name,
message: error.message,
circular: true,
};
}
seen.add(error);
const result = {
name: error.name,
message: error.message,
};
if (
typeof error.stack
=== "string"
) {
result.stack = error.stack;
}
if (
Object.hasOwn(
error,
"code",
)
) {
result.code = error.code;
}
if (
Object.hasOwn(
error,
"cause",
)
) {
result.cause
= serializeError(
error.cause,
seen,
);
}
if (
error
instanceof AggregateError
) {
result.errors
= error.errors.map(
item => (
serializeError(
item,
seen,
)
),
);
}
return result;
}
生产环境还应:
- 限制 cause 深度;
- 限制 AggregateError 数量;
- 截断超长 message 和 stack;
- 清理敏感字段;
- 防止 getter 在序列化时抛错;
- 避免把整个任意对象直接上传。
8.23 面向用户的错误信息
8.23.1 用户信息与技术信息分离
技术日志:
POST /api/orders returned 503
after 3 retries
用户提示:
暂时无法提交订单,请稍后重试。
不要直接展示:
error.stack
也不要把数据库、服务器路径、内部服务名称或安全细节暴露给用户。
8.23.2 信息应可行动
较差:
发生错误。
较好:
文件无法上传。请检查网络连接,或稍后重试。
如果用户无法采取任何行动,应说明系统接下来会做什么:
保存失败。内容已保留在本地草稿中。
8.23.3 不应伪造成功
try {
await saveData();
} catch {
showMessage(
"保存成功",
);
}
错误降级不能通过误导用户实现。即使后台会重试,也应准确表达当前状态:
暂时无法同步,数据已保存在本地,恢复连接后将再次尝试。
8.24 错误边界
错误边界是能够统一接收下层未处理错误的架构层级。
浏览器应用中的边界可能包括:
- 页面入口;
- 路由;
- UI 组件树;
- 用户操作处理器;
- Worker 消息处理器;
- 网络请求层;
- 后台任务调度器。
服务端边界可能包括:
- 单个 HTTP 请求;
- 消息队列消费;
- 定时任务;
- CLI 命令;
- Worker 线程;
- 进程顶层。
8.24.1 请求级边界
async function handleRequest(
request,
) {
try {
return await routeRequest(
request,
);
} catch (error) {
const normalized
= toError(error);
reportError(
normalized,
{
requestId:
request.id,
},
);
return createErrorResponse(
normalized,
);
}
}
边界负责:
- 统一日志;
- 转换外部响应;
- 防止错误污染其他请求;
- 保证必要清理;
- 关联请求上下文。
8.24.2 边界不应无条件继续
某些错误表示程序状态已经不可信:
- 核心不变量破坏;
- 内存或资源耗尽;
- 初始化配置错误;
- 安全边界失效;
- 数据库事务状态未知。
此时继续运行可能产生更大损害。边界应根据系统类型决定:
- 终止当前操作;
- 重新加载页面;
- 关闭 Worker;
- 优雅停止服务;
- 交由进程管理器重启。
8.25 断言与不变量
8.25.1 断言函数
function invariant(
condition,
message,
) {
if (!condition) {
throw new Error(
`Invariant violation: ${message}`,
);
}
}
使用:
invariant(
currentUser !== null,
"currentUser 必须已经初始化",
);
断言用于检测开发者认为必然成立的内部条件,不应代替普通用户输入验证。
8.25.2 输入验证与断言的区别
用户输入错误:
throw new ValidationError(
"email 格式无效",
);
程序不变量错误:
throw new Error(
"Invariant violation: "
+ "order total is negative",
);
前者通常可以向用户反馈并继续操作;后者通常意味着程序或数据逻辑存在缺陷,需要上报和中止当前流程。
8.25.3 console.assert
console.assert(
condition,
"message",
);
console.assert() 属于宿主调试接口,通常只输出信息,不会稳定地抛出异常。不能用它维护生产代码不变量。
8.26 测试错误路径
8.26.1 测试同步异常
function expectThrows(
action,
ErrorType,
) {
let thrown = false;
try {
action();
} catch (error) {
thrown = true;
if (
!(
error
instanceof ErrorType
)
) {
throw new Error(
"抛出了错误类型,"
+ "但不是预期类型",
{
cause: error,
},
);
}
}
if (!thrown) {
throw new Error(
"预期函数抛出异常",
);
}
}
实际项目应使用测试框架提供的错误断言。
8.26.2 测试 Promise 拒绝
不正确:
// 测试可能在 Promise 完成前结束。
saveData().catch(
() => {
// assertion
},
);
应返回或等待 Promise:
await assertRejects(
() => saveData(),
NetworkError,
);
8.26.3 测试 cause
try {
await loadConfiguration();
} catch (error) {
assert(
error
instanceof ConfigurationError,
);
assert(
error.cause
instanceof SyntaxError,
);
}
8.26.4 测试 finally 清理
let closed = false;
const resource = {
close() {
closed = true;
},
};
try {
useResource(resource);
} catch {
// 预期错误。
} finally {
resource.close();
}
assert(closed);
错误路径不能只靠人工阅读保证,应对:
- 成功;
- 预期失败;
- 未知失败;
- 取消;
- 超时;
- 清理失败;
- 多错误聚合。
分别编写测试。
8.27 常见错误处理问题
8.27.1 空 catch
try {
performOperation();
} catch {}
这会完全隐藏错误。只有在失败确实可以无条件忽略,并且该行为已经被清楚记录时才可使用。
8.27.2 捕获后只打印
try {
return await loadData();
} catch (error) {
console.error(error);
}
函数失败时变为返回 undefined。调用者通常无法区分失败和正常空值。
8.27.3 抛出字符串
throw "failed";
缺少统一错误结构和调用栈支持。应使用:
throw new Error("failed");
8.27.4 解析 message
if (
error.message.includes(
"not found",
)
) {
// ...
}
信息文本不是稳定接口。使用类、code 或结构化字段。
8.27.5 所有错误都转为同一错误
catch (error) {
throw new Error(
"Operation failed",
);
}
如果没有 cause,底层信息丢失。即使有 cause,也不应在每一层无意义包装。
8.27.6 重复日志
catch (error) {
console.error(error);
throw error;
}
多层重复会制造噪声。应明确由哪个错误边界负责最终记录。
8.27.7 把业务分支全部写成异常
try {
return getCachedValue(key);
} catch {
return computeValue(key);
}
如果“缓存未命中”是正常情况,更适合返回 undefined、null 或 Result,而不是频繁构造异常。
8.27.8 过度依赖 instanceof
跨 Realm、重复安装的包副本或不同构建上下文,可能导致看似同类的错误无法通过当前类的 instanceof。
公共库可以同时提供:
- 稳定错误
code; - 类类型;
name;- 明确文档。
ECMAScript 2026 的 Error.isError() 只判断是否为标准 Error 对象,不会判断是否属于某个自定义错误类别。
8.27.9 忽略清理错误
finally {
await resource.close();
}
如果关闭失败,它可能覆盖原操作错误。关键资源应明确设计清理失败策略。
8.27.10 把全局错误监听当作恢复机制
未捕获异常发生后,应用状态可能已经部分更新。全局处理器通常只适合记录、展示故障页或终止当前边界,而不是假设程序可以无条件继续。
8.28 完整示例:配置加载错误链
8.28.1 错误类型
class ApplicationError
extends Error {
constructor(
message,
{
code,
cause,
details,
} = {},
) {
super(
message,
{
cause,
},
);
this.name = (
this.constructor.name
);
this.code = code;
this.details = details;
}
}
class HttpError
extends ApplicationError {}
class ConfigurationError
extends ApplicationError {}
class ConfigurationValidationError
extends ConfigurationError {}
8.28.2 请求层
async function fetchText(
url,
{
signal,
} = {},
) {
let response;
try {
response = await fetch(
url,
{
signal,
},
);
} catch (error) {
if (
signal?.aborted
) {
throw error;
}
throw new HttpError(
"网络请求失败",
{
code: "NETWORK_FAILURE",
cause: error,
details: {
url,
},
},
);
}
if (!response.ok) {
throw new HttpError(
`服务器返回 HTTP ${
response.status
}`,
{
code: "HTTP_STATUS_ERROR",
details: {
url,
status: response.status,
},
},
);
}
return response.text();
}
8.28.3 解析层
function parseConfiguration(
sourceText,
) {
let data;
try {
data = JSON.parse(
sourceText,
);
} catch (error) {
throw new ConfigurationError(
"配置文件不是合法 JSON",
{
code: "CONFIG_SYNTAX_ERROR",
cause: error,
},
);
}
if (
data === null
|| typeof data !== "object"
|| Array.isArray(data)
) {
throw (
new ConfigurationValidationError(
"配置根节点必须是对象",
{
code: "CONFIG_INVALID_ROOT",
details: {
receivedType:
data === null
? "null"
: typeof data,
},
},
)
);
}
if (
typeof data.apiBaseUrl
!== "string"
) {
throw (
new ConfigurationValidationError(
"配置缺少 apiBaseUrl",
{
code:
"CONFIG_MISSING_API_URL",
details: {
field: "apiBaseUrl",
},
},
)
);
}
return data;
}
8.28.4 业务层
async function loadConfiguration(
url,
options,
) {
try {
const sourceText
= await fetchText(
url,
options,
);
return parseConfiguration(
sourceText,
);
} catch (error) {
if (
error
instanceof ConfigurationError
) {
throw error;
}
throw new ConfigurationError(
"无法加载应用配置",
{
code: "CONFIG_LOAD_FAILED",
cause: error,
details: {
url,
},
},
);
}
}
8.28.5 顶层边界
async function startApplication() {
const controller
= new AbortController();
try {
const configuration
= await loadConfiguration(
"/config.json",
{
signal:
controller.signal,
},
);
await initializeApplication(
configuration,
);
} catch (error) {
const normalized
= toError(error);
reportError(
normalized,
{
operation:
"application-startup",
},
);
showFatalError(
"应用暂时无法启动。"
+ "请稍后重新加载页面。",
);
}
}
这个分层结构具有以下特点:
- 请求层负责网络和 HTTP 语义;
- 解析层负责 JSON 和字段验证;
- 业务层负责统一配置加载语义;
- 顶层边界负责日志和用户提示;
cause保留完整底层原因;- 错误代码供程序分支和监控聚合;
- 用户界面不直接暴露内部技术细节。
8.29 错误处理设计原则
8.29.1 失败契约应明确
函数应说明:
- 会抛出哪些错误;
- 哪些失败通过返回值表示;
- 是否可能取消;
- 是否自动重试;
- 是否会包装底层错误;
- 调用者是否需要清理资源。
8.29.2 捕获范围应尽量小
只把预期可能失败、并且能够准确分类的语句放入 try。
范围越大,越容易无意捕获与当前处理无关的程序错误。
8.29.3 在有能力恢复的层级处理
底层通常负责:
- 检测失败;
- 添加技术上下文;
- 保留
cause。
高层通常负责:
- 重试;
- 降级;
- 用户提示;
- 终止操作;
- 统一日志。
8.29.4 不要丢失原始原因
使用:
new Error(
"High-level message",
{
cause: originalError,
},
);
而不是只复制 message。
8.29.5 不要隐藏未知错误
只处理明确识别的错误,其他错误重新抛出。
8.29.6 清理与业务逻辑分离
finally 负责资源释放,不负责决定成功结果。
8.29.7 日志与用户提示分离
日志用于诊断,用户提示用于帮助用户采取下一步行动。
8.29.8 对错误路径进行测试
错误处理不是附加功能,而是程序主要控制路径的一部分。
8.30 本章小结
本章系统介绍了 JavaScript 的错误对象与异常控制流:
- JavaScript 的许多异常结果会返回
NaN、Infinity或undefined,不会自动抛出错误; throw可以抛出任意值,但工程代码应优先抛出 Error 或其子类;throw与后续表达式之间不能换行;try...catch只能捕获其动态执行范围内的抛出完成;- 外层同步
try...catch无法捕获未来定时器回调中的异常; - Promise 拒绝不是当前调用点的同步 throw,必须通过
.catch()或await处理; finally会在返回或异常继续传播前执行;finally中的return或throw会覆盖原来的返回值或错误;- Error 标准属性主要包括
name、message和可选的cause; stack被广泛实现但不属于 ECMAScript 2026 的标准属性,不能依赖固定格式;Error.captureStackTrace()是 V8 专有接口,需要功能检测;Error.cause用于保留底层失败原因,比拼接错误信息更加结构化;- ECMAScript 2026 的
Error.isError()根据标准内部错误标记识别 Error,对跨 Realm 场景比instanceof Error更可靠; Error.isError()是新接口,旧环境可能需要近似后备方案;- TypeError、RangeError、ReferenceError、SyntaxError 和 URIError 分别表达不同原生失败类型;
- EvalError 在现代规范中保留用于兼容,但规范不主动使用它;
- 源文件自身的静态语法错误通常无法由同一文件中的
try...catch捕获; - AggregateError 通过
errors属性保存多个失败,适合批量和并发操作; - 自定义错误类应提供稳定类型、可选错误代码和结构化字段;
- 错误文本不应成为程序分支协议,优先使用类型、
code和字段; - 当前层只应处理能够恢复或转换语义的错误,未知错误应继续抛出;
- 捕获范围越大,越容易无意吞掉程序缺陷;
- Promise
.catch()正常返回会使链恢复为 fulfilled; return await在需要当前try...catch...finally覆盖 Promise 拒绝时具有明确用途;- 未等待的 Promise 必须有独立拒绝处理策略;
- 浏览器的
error和unhandledrejection事件适合作为最终遥测边界,而不是替代局部处理; - 资源应在成功、失败和取消路径中统一清理,并注意清理错误可能覆盖原错误;
- Promise 竞争只改变聚合结果,不会自动取消底层操作;
- 重试只适合暂时性且可以安全重放的失败;
- Result 模式适合频繁、预期并要求调用者显式分支处理的失败;
- 错误日志应包含必要上下文,但必须避免记录敏感数据;
- 面向用户的错误信息应准确、可行动,并与内部技术日志分离;
- 错误边界负责统一记录、转换外部响应和终止受影响的操作;
- 错误路径、取消路径和清理路径都应通过自动化测试验证。
参考资料
- javascript.info: Error handling, try…catch
- javascript.info: Custom errors, extending Error
- javascript.info: Promises chaining
- javascript.info: Error handling with promises
- javascript.info: Async/await
- MDN JavaScript Guide: Control flow and error handling
- MDN JavaScript Reference: try…catch
- MDN JavaScript Reference: throw
- MDN JavaScript Reference: Error
- MDN JavaScript Reference: Error.cause
- MDN JavaScript Reference: AggregateError
- MDN JavaScript Reference: Error.isError
- MDN Web API: Window error event
- MDN Web API: Window unhandledrejection event
- ECMAScript 2026 Language Specification: The throw Statement
- ECMAScript 2026 Language Specification: The try Statement
- ECMAScript 2026 Language Specification: Error Objects
- ECMAScript 2026 Language Specification: Error.isError
- ECMAScript 2026 Language Specification: AggregateError Objects
- ECMAScript 2026 Language Specification: InstallErrorCause