错误处理机制与边界防御
Site Owner
Published on 2026-05-21
系统讲解try/catch/finally完整语义、自定义错误类型设计、Promise unhandledrejection事件及Error.cause链式追踪

错误处理机制与边界防御
同步异常与 try/catch 的捕获边界;异步错误的传播链路(Promise rejection / async-await);全局兜底机制(window.onerror / unhandledrejection);React Error Boundary 与 Vue onErrorCaptured 的组件级错误隔离;以及错误信息的标准化上报设计。
目录
同步错误与 try/catch
捕获边界
// ✅ try/catch 可以捕获同步错误
try {
JSON.parse('invalid json');
null.property; // TypeError
undeclaredVar; // ReferenceError
} catch (e) {
console.log(e instanceof SyntaxError); // true
console.log(e.name); // "SyntaxError"
console.log(e.message); // "Unexpected token..."
console.log(e.stack); // 调用栈
}
// ❌ try/catch 无法捕获异步错误
try {
setTimeout(() => {
throw new Error('异步错误'); // 无法被捕获!
}, 100);
} catch (e) {
console.log('不会执行');
}
// ❌ 无法捕获 Promise rejection(同步 throw)
try {
Promise.reject(new Error('rejected')); // 无法被捕获!
} catch (e) {
console.log('不会执行');
}
Error 对象的标准属性
try {
throw new Error('Something went wrong');
} catch (e) {
console.log(e.name); // "Error"
console.log(e.message); // "Something went wrong"
console.log(e.stack); // 含文件名、行号、列号的调用栈字符串
// 非标准但广泛支持
console.log(e.fileName); // (Firefox)
console.log(e.lineNumber); // (Firefox)
console.log(e.columnNumber);
}
// 内置错误类型
const errors = {
SyntaxError: '语法错误(JSON.parse, eval 等)',
TypeError: '类型错误(null.x, 非函数调用)',
ReferenceError: '引用错误(未声明变量)',
RangeError: '范围错误(new Array(-1))',
URIError: 'URI 错误(decodeURIComponent 格式错误)',
EvalError: 'eval 错误(已弃用)',
};
finally 的陷阱
function dangerous() {
try {
throw new Error('原始错误');
} finally {
// ❌ finally 中 return 会"吞掉"异常!
return 'finally 的返回值'; // 原始错误被忽略
}
}
console.log(dangerous()); // 'finally 的返回值',不会抛出错误
// ❌ finally 中 throw 会替换原始异常
function dangerous2() {
try {
throw new Error('原始错误');
} finally {
throw new Error('finally 的错误'); // 原始错误丢失!
}
}
异步错误传播
Promise 错误链
// Promise 的错误会沿链传播
Promise.resolve()
.then(() => { throw new Error('step 1 error'); })
.then(() => console.log('不执行'))
.then(() => console.log('不执行'))
.catch(e => console.log('捕获到:', e.message)) // ✅ 捕获到
.then(() => console.log('恢复执行')); // ✅ 继续执行
// 在 catch 中 re-throw
fetch('/api/data')
.then(r => r.json())
.catch(e => {
if (e.name === 'SyntaxError') {
throw new TypeError('响应不是有效的 JSON');
}
throw e; // 其他错误继续传播
})
.catch(e => console.error('最终错误:', e));
async/await 错误处理
// await 会将 Promise rejection 转为同步 throw
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new HttpError(response.status, response.statusText);
}
const data = await response.json(); // JSON 解析失败会被 catch 捕获
return data;
} catch (e) {
if (e instanceof HttpError) {
console.error(`HTTP 错误 ${e.status}: ${e.message}`);
return null;
}
throw e; // 非预期错误,继续向上传播
}
}
// 并发请求的错误处理
async function fetchMultiple(urls) {
// Promise.all — 任一失败即整体失败
try {
const results = await Promise.all(urls.map(url => fetch(url).then(r => r.json())));
return results;
} catch (e) {
console.error('至少一个请求失败');
}
// Promise.allSettled — 等待所有完成,不因单个失败而中止
const results = await Promise.allSettled(urls.map(url => fetch(url).then(r => r.json())));
return results.map(result => {
if (result.status === 'fulfilled') return { ok: true, data: result.value };
return { ok: false, error: result.reason.message };
});
}
常见的 async 错误陷阱
// ❌ 陷阱1:forEach 中的 async 错误无法被外层 try/catch 捕获
async function bad() {
try {
[1, 2, 3].forEach(async (n) => {
await doSomething(n); // 这里的错误无法被捕获!
});
} catch (e) {
console.log('不会执行');
}
}
// ✅ 修复:使用 for...of 或 Promise.all
async function good() {
try {
for (const n of [1, 2, 3]) {
await doSomething(n);
}
// 或:
await Promise.all([1, 2, 3].map(n => doSomething(n)));
} catch (e) {
console.log('能捕获到');
}
}
// ❌ 陷阱2:未 await 的 Promise 错误丢失
async function noAwait() {
const p = fetch('/may-fail'); // 忘记 await,错误丢失!
return '完成';
}
// ❌ 陷阱3:async 函数的同步部分在 try 外
async function asyncTrap() {
const data = JSON.parse('{invalid'); // 这个 throw 在 Promise 构造前,会变成 rejected Promise
// 但调用方需要 await 或 .catch 才能捕获
}
全局错误兜底
浏览器全局错误监听
// 同步错误 + 某些资源加载错误
window.onerror = function(message, source, lineno, colno, error) {
reportError({
type: 'js-error',
message,
source, // 脚本 URL
lineno,
colno,
stack: error?.stack
});
return true; // 阻止默认的控制台输出
};
// 更现代的方式(可多次监听,不被覆盖)
window.addEventListener('error', (event) => {
if (event.target instanceof HTMLElement) {
// 资源加载错误(img, script, link 等)
reportError({
type: 'resource-error',
tag: event.target.tagName,
src: event.target.src || event.target.href,
});
return;
}
// 运行时错误(与 onerror 同)
}, true); // 必须在捕获阶段!资源加载错误不冒泡
// 未处理的 Promise rejection
window.addEventListener('unhandledrejection', (event) => {
event.preventDefault(); // 阻止控制台警告(可选)
reportError({
type: 'unhandled-promise',
reason: event.reason?.message ?? String(event.reason),
stack: event.reason?.stack
});
});
// Promise rejection 被处理(之前报告过可以撤销)
window.addEventListener('rejectionhandled', (event) => {
console.log('Promise rejection 已被处理:', event.promise);
});
Node.js 全局错误
// 未捕获的同步异常
process.on('uncaughtException', (error, origin) => {
// ⚠️ 此时进程状态可能已损坏,最好只做日志记录后退出
console.error(`未捕获异常 [${origin}]:`, error);
process.exit(1); // 强烈建议退出,不要继续运行
});
// 未处理的 Promise rejection
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise rejection:', reason);
// Node 18+ 默认会使进程崩溃
});
// 优雅退出信号
process.on('SIGTERM', async () => {
console.log('收到 SIGTERM,开始优雅关闭...');
await server.close();
await database.disconnect();
process.exit(0);
});
自定义 Error 类型系统
// 基础错误基类
class AppError extends Error {
constructor(message, options = {}) {
super(message);
this.name = this.constructor.name;
this.code = options.code ?? 'UNKNOWN_ERROR';
this.statusCode = options.statusCode ?? 500;
this.isOperational = options.isOperational ?? true; // 业务错误 vs 系统错误
this.context = options.context ?? {}; // 附加上下文信息
// 保持正确的 stack trace(V8 专用)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
toJSON() {
return {
name: this.name,
code: this.code,
message: this.message,
statusCode: this.statusCode,
context: this.context
};
}
}
// 具体错误类型
class ValidationError extends AppError {
constructor(message, fields = {}) {
super(message, { code: 'VALIDATION_ERROR', statusCode: 400 });
this.fields = fields; // { fieldName: errorMessage }
}
}
class NetworkError extends AppError {
constructor(message, { url, status } = {}) {
super(message, { code: 'NETWORK_ERROR', statusCode: status ?? 503 });
this.url = url;
this.status = status;
}
}
class AuthError extends AppError {
constructor(message = '认证失败') {
super(message, { code: 'AUTH_ERROR', statusCode: 401 });
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} 不存在`, { code: 'NOT_FOUND', statusCode: 404 });
this.resource = resource;
}
}
// 错误类型判断工具
function isOperationalError(error) {
return error instanceof AppError && error.isOperational;
}
// 集中式错误处理器
function handleError(error) {
if (error instanceof ValidationError) {
return { success: false, errors: error.fields };
}
if (error instanceof AuthError) {
redirectToLogin();
return;
}
if (error instanceof NetworkError && error.status >= 500) {
showServerErrorToast();
}
if (!isOperationalError(error)) {
// 系统级错误(bug),需要特殊处理
reportCritical(error);
}
throw error;
}
组件级错误边界
React Error Boundary
// 类组件实现错误边界
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
// 渲染阶段捕获错误,用于更新 state
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
// 提交阶段,可执行副作用(如上报)
componentDidCatch(error, errorInfo) {
console.error('组件错误:', error, errorInfo.componentStack);
reportError({
type: 'react-component-error',
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack
});
}
handleReset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return this.props.fallback
? this.props.fallback(this.state.error, this.handleReset)
: <div>Something went wrong. <button onClick={this.handleReset}>Retry</button></div>;
}
return this.props.children;
}
}
// 使用
function App() {
return (
<ErrorBoundary
fallback={(error, reset) => (
<ErrorPage error={error} onRetry={reset} />
)}
>
<UserProfile />
</ErrorBoundary>
);
}
// ⚠️ Error Boundary 无法捕获:
// - 事件处理器中的错误(需手动 try/catch)
// - 异步代码(setTimeout、Promise)
// - 服务端渲染中的错误
// - Error Boundary 自身的错误
Vue 的错误处理钩子
// Vue 3 全局错误处理
const app = createApp(App);
app.config.errorHandler = (error, instance, info) => {
// error: 错误对象
// instance: 触发错误的组件实例
// info: Vue 特定的错误信息(如生命周期钩子名称)
console.error(`[Vue Error] in ${info}:`, error);
reportError({ type: 'vue-error', message: error.message, info });
};
// 组件级:onErrorCaptured(类似 React Error Boundary)
export default {
setup() {
onErrorCaptured((error, instance, info) => {
console.error('子组件错误:', error);
reportError({ message: error.message, info });
// 返回 false 阻止错误继续向上传播
// 不返回(或返回 true)则继续传播
return false;
});
}
};
错误监控上报
标准化错误上报
class ErrorReporter {
#queue = [];
#flushing = false;
#maxQueueSize = 50;
#endpoint;
constructor(endpoint) {
this.#endpoint = endpoint;
// 页面卸载时发送剩余错误
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.#flushSync();
}
});
}
report(error, context = {}) {
if (this.#queue.length >= this.#maxQueueSize) {
this.#queue.shift(); // 丢弃最早的
}
this.#queue.push({
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent,
error: this.#serializeError(error),
context,
// 性能快照
memory: performance.memory ? {
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
} : null,
});
this.#scheduleFlush();
}
#serializeError(error) {
if (error instanceof Error) {
return {
name: error.name,
message: error.message,
stack: this.#parseStack(error.stack),
code: error.code,
};
}
return { message: String(error) };
}
#parseStack(stack = '') {
// 解析 stack 为结构化数据
return stack.split('\n').slice(1).map(line => {
const match = line.match(/at (.+?) \((.+?):(\d+):(\d+)\)/);
if (match) {
return { fn: match[1], file: match[2], line: +match[3], col: +match[4] };
}
return { raw: line.trim() };
});
}
#scheduleFlush() {
if (!this.#flushing) {
this.#flushing = true;
// 批量发送,延迟 2s 聚合更多错误
setTimeout(() => this.#flush(), 2000);
}
}
async #flush() {
this.#flushing = false;
if (!this.#queue.length) return;
const batch = this.#queue.splice(0);
try {
await fetch(this.#endpoint, {
method: 'POST',
body: JSON.stringify({ errors: batch }),
headers: { 'Content-Type': 'application/json' },
keepalive: true // 允许页面卸载后继续发送
});
} catch {
// 发送失败,放回队列
this.#queue.unshift(...batch);
}
}
// 页面卸载时同步发送(sendBeacon 不受页面卸载影响)
#flushSync() {
if (!this.#queue.length) return;
const batch = this.#queue.splice(0);
navigator.sendBeacon(
this.#endpoint,
new Blob([JSON.stringify({ errors: batch })], { type: 'application/json' })
);
}
}
// 全局实例
const reporter = new ErrorReporter('/api/errors');
// 挂载全局监听
window.addEventListener('error', e => reporter.report(e.error ?? e));
window.addEventListener('unhandledrejection', e => reporter.report(e.reason));
实战案例
带错误边界的 API 调用层
class ApiClient {
async call(fn, options = {}) {
const {
retries = 0,
fallback = null,
timeout = 10000,
errorTransform = e => e
} = options;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const result = await fn(controller.signal);
clearTimeout(timer);
return result;
} catch (e) {
clearTimeout(timer);
if (e.name === 'AbortError') throw new NetworkError('请求超时', { status: 408 });
throw e;
}
} catch (e) {
const transformed = errorTransform(e);
// 最后一次尝试
if (attempt === retries) {
if (fallback !== null) {
console.warn('使用降级数据:', transformed.message);
return typeof fallback === 'function' ? fallback(transformed) : fallback;
}
throw transformed;
}
// 非网络错误不重试
if (!(e instanceof NetworkError)) throw transformed;
await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
}
}
}
}
深度追问
Q1:async/await 中,哪些错误无法被 try/catch 捕获?
① new Promise 构造函数中的同步异常会转为 rejection,需要 await/.catch 处理;② setTimeout/setInterval 内的异常完全逃脱 async 的错误边界;③ Promise.all 中某个 Promise 的 rejection 会被 await Promise.all(...) 的 try/catch 捕获,但其余 Promise 继续执行(不会被取消)。
Q2:window.onerror 能捕获跨域脚本的错误吗?
不能。跨域脚本的错误信息会被浏览器隐藏(安全策略),onerror 只会收到 "Script error." / null / 0 / 0。解决方案:① 给脚本标签添加 crossorigin="anonymous" 属性;② 在 CDN 服务器设置 Access-Control-Allow-Origin: * 响应头。
Q3:React Error Boundary 为什么不能是函数组件?
Error Boundary 依赖 getDerivedStateFromError 和 componentDidCatch 两个类组件生命周期方法。函数组件目前没有等价的 Hook(React 团队有计划但尚未实现)。实践中可以封装成 HOC 或使用第三方库(如 react-error-boundary)以函数组件方式使用。
Q4:finally 块中 return 为什么会"吞掉"异常?
ECMAScript 规范规定:finally 中的 return / throw 会覆盖 try/catch 中产生的 completion record(包括异常)。这是语言设计,开发者需避免在 finally 中使用 return,或明确意识到其语义。