Back to list错误处理机制与边界防御
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
- 异步错误传播
- 全局错误兜底
- 自定义 Error 类型系统
- 组件级错误边界
- 错误监控上报
- 实战案例
- 深度追问
同步错误与 try/catch
捕获边界
try {
JSON.parse('invalid json');
null.property;
undeclaredVar;
} (e) {
.(e );
.(e.);
.(e.);
.(e.);
}
{
( {
();
}, );
} (e) {
.();
}
{
.( ());
} (e) {
.();
}
JavaScript错误处理机制—try/catch语义、异步错误捕获与Error.cause|新宇宙博客catch
console
log
instanceof
SyntaxError
console
log
name
console
log
message
console
log
stack
try
setTimeout
() =>
throw
new
Error
'异步错误'
100
catch
console
log
'不会执行'
try
Promise
reject
new
Error
'rejected'
catch
console
log
'不会执行'
Error 对象的标准属性
try {
throw new Error('Something went wrong');
} catch (e) {
console.log(e.name);
console.log(e.message);
console.log(e.stack);
console.log(e.fileName);
console.log(e.lineNumber);
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 {
return 'finally 的返回值';
}
}
console.log(dangerous());
function dangerous2() {
try {
throw new Error('原始错误');
} finally {
throw new Error('finally 的错误');
}
}
异步错误传播
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('恢复执行'));
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 错误处理
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();
return data;
} catch (e) {
if (e instanceof HttpError) {
console.error(`HTTP 错误 ${e.status}: ${e.message}`);
return null;
}
throw e;
}
}
async function fetchMultiple(urls) {
try {
const results = await Promise.all(urls.map(url => fetch(url).then(r => r.json())));
return results;
} catch (e) {
console.error('至少一个请求失败');
}
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 错误陷阱
async function bad() {
try {
[1, 2, 3].forEach(async (n) => {
await doSomething(n);
});
} catch (e) {
console.log('不会执行');
}
}
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('能捕获到');
}
}
async function noAwait() {
const p = fetch('/may-fail');
return '完成';
}
async function asyncTrap() {
const data = JSON.parse('{invalid');
}
全局错误兜底
浏览器全局错误监听
window.onerror = function(message, source, lineno, colno, error) {
reportError({
type: 'js-error',
message,
source,
lineno,
colno,
stack: error?.stack
});
return true;
};
window.addEventListener('error', (event) => {
if (event.target instanceof HTMLElement) {
reportError({
type: 'resource-error',
tag: event.target.tagName,
src: event.target.src || event.target.href,
});
return;
}
}, true);
window.addEventListener('unhandledrejection', (event) => {
event.preventDefault();
reportError({
type: 'unhandled-promise',
reason: event.reason?.message ?? String(event.reason),
stack: event.reason?.stack
});
});
window.addEventListener('rejectionhandled', (event) => {
console.log('Promise rejection 已被处理:', event.promise);
});
Node.js 全局错误
process.on('uncaughtException', (error, origin) => {
console.error(`未捕获异常 [${origin}]:`, error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise rejection:', reason);
});
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;
this.context = options.context ?? {};
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;
}
}
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)) {
reportCritical(error);
}
throw error;
}
组件级错误边界
React Error Boundary
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
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>
);
}
Vue 的错误处理钩子
const app = createApp(App);
app.config.errorHandler = (error, instance, info) => {
console.error(`[Vue Error] in ${info}:`, error);
reportError({ type: 'vue-error', message: error.message, info });
};
export default {
setup() {
onErrorCaptured((error, instance, info) => {
console.error('子组件错误:', error);
reportError({ message: error.message, info });
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 = '') {
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;
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);
}
}
#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,或明确意识到其语义。