在现代 JavaScript 开发中,合理的异常处理是构建健壮应用的基石。本文将探讨异常的本质特征、标准错误类型、捕获机制以及异步场景下的处理策略,帮助你建立完整的异常处理知识体系。
异常的本质
异常(Exception)是程序执行过程中发生的意外情况,它会打断正常的执行流程。理解异常的核心特征对于编写健壮的代码至关重要:
- 中断执行流(Interrupting Flow): 异常发生时,代码会立即停止执行当前语句及后续代码
- 向上传播(Propagation): 异常会沿着函数调用栈逐层向上传播,直到被捕获或到达顶层导致程序崩溃
- 包含上下文(Context): 异常对象携带丰富的调试信息,包括错误消息、堆栈跟踪、错误类型等
- 可恢复性(Recoverability): 通过 try-catch 机制捕获和处理异常,程序可以从错误状态中恢复并继续执行
JavaScript 中的标准错误类型
JavaScript 提供了完整的内置错误类型体系,每种错误类型对应特定的异常场景,理解它们有助于更精准地处理错误。
javascript
Error (基类)
├── SyntaxError # 语法错误
├── ReferenceError # 引用错误
├── TypeError # 类型错误
├── RangeError # 范围错误
├── URIError # URI 错误
├── EvalError # eval() 错误(已废弃)
└── AggregateError # 聚合错误(ES2021)
1. SyntaxError - 语法错误
语法错误在代码解析阶段就会抛出,通常无法通过 try-catch 捕获(除非在 eval 或 JSON.parse 等运行时解析的场景)。
javascript
// ❌ 无法捕获 - 代码根本无法运行
try {
eval('function {') // SyntaxError: Unexpected token '{'
} catch (e) {
// 可以捕获 eval 内部的语法错误
console.error(e)
}
// ❌ 无法捕获 - 解析阶段就报错
try {
const x = ; // SyntaxError: Unexpected token ';'
} catch (e) {
// 这段代码永远不会执行
}
// ✅ JSON 解析错误可以捕获
try {
JSON.parse('{invalid json}') // SyntaxError
} catch (e) {
console.error('JSON 解析失败')
}
2. ReferenceError - 引用错误
当尝试访问未声明或不存在的变量时抛出,这是变量作用域问题的最直接体现。
javascript
try {
console.log(undeclaredVariable) // ReferenceError
} catch (e) {
console.error('变量未定义:', e.message)
}
// 严格模式下的特殊情况
'use strict'
try {
unknownVar = 10 // ReferenceError: unknownVar is not defined
} catch (e) {
console.error(e)
}
// 非严格模式下会自动创建全局变量(不推荐)
unknownVar2 = 20 // 不抛出错误,创建 window.unknownVar2
3. TypeError - 类型错误
当值的类型不符合预期操作时抛出,这是 JavaScript 中最常见的运行时错误,也是类型系统动态性带来的主要挑战。
javascript
// 对非函数调用
try {
const num = 42
num() // TypeError: num is not a function
} catch (e) {
console.error(e)
}
// 访问 null/undefined 的属性
try {
const obj = null
obj.property // TypeError: Cannot read properties of null
} catch (e) {
console.error(e)
}
// 对常量重新赋值
try {
const constant = 10
constant = 20 // TypeError: Assignment to constant variable
} catch (e) {
console.error(e)
}
// 对不可扩展对象添加属性
try {
const obj = Object.freeze({ name: 'Alice' })
obj.age = 30 // 严格模式下 TypeError
} catch (e) {
console.error(e)
}
4. RangeError - 范围错误
当数值或数据结构的大小超出有效范围时抛出,常见于递归调用、数组操作等场景。
javascript
// 递归调用栈溢出
function infiniteRecursion() {
infiniteRecursion() // RangeError: Maximum call stack size exceeded
}
try {
infiniteRecursion()
} catch (e) {
console.error('栈溢出:', e.message)
}
// 数组长度无效
try {
const arr = new Array(-1) // RangeError: Invalid array length
} catch (e) {
console.error(e)
}
// 数字精度超出范围
try {
const num = (123.456).toFixed(101) // RangeError
} catch (e) {
console.error(e)
}
5. AggregateError - 聚合错误 (ES2021)
ES2021 引入的新错误类型,用于表示多个错误的集合,主要用于 Promise.any()
等需要聚合多个失败结果的场景。
javascript
// Promise.any() 所有 Promise 都失败时抛出
try {
await Promise.any([
Promise.reject(new Error('错误1')),
Promise.reject(new Error('错误2')),
Promise.reject(new Error('错误3')),
])
} catch (e) {
console.log(e instanceof AggregateError) // true
console.log(e.errors) // [Error: 错误1, Error: 错误2, Error: 错误3]
}
// 手动创建 AggregateError
throw new AggregateError(
[
new Error('验证失败: 用户名不能为空'),
new Error('验证失败: 密码长度不足'),
],
'表单验证失败'
)
异常捕获机制
同步代码中的异常捕获
同步代码使用经典的 try-catch-finally 结构,这是最基础也是最常用的异常处理方式。
javascript
// 基础用法
try {
// 可能抛出异常的代码
const result = riskyOperation()
} catch (error) {
// 处理异常
console.error('操作失败:', error.message)
} finally {
// 无论是否发生异常都会执行
cleanup()
}
Promise 与 async/await 中的异常处理
Promise 和 async/await 本质上是一回事 ------ async 函数总是返回 Promise 。它们提供了两种主要的异常处理方式:.catch()
方法用于链式调用,try-catch
结构配合 async/await 提供更直观的同步风格。
javascript
// 方式 1: .catch() 链式调用
fetch('/api/user')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json()
})
.then(user => {
console.log('用户:', user)
})
.catch(error => {
// 捕获链中任何一步的异常
console.error('请求失败:', error)
})
.finally(() => {
// 无论成功失败都执行
hideLoadingSpinner()
})
// 方式 2: async/await + try-catch (推荐)
async function loadUser() {
try {
const response = await fetch('/api/user')
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const user = await response.json()
console.log('用户:', user)
return user
} catch (error) {
console.error('请求失败:', error)
throw error // 重新抛出,让调用者决定如何处理
} finally {
hideLoadingSpinner()
}
}
// 方式 3: Promise.then() 的第二个参数(不推荐)
fetch('/api/data')
.then(
(data) => {
// 成功回调
console.log(data)
},
(error) => {
// 失败回调 - 仅捕获 fetch 本身的错误
// 无法捕获成功回调中的异常!
console.error(error)
}
)
未处理的 Promise Rejection
未处理的 Promise rejection 会触发全局事件,在浏览器和 Node.js 环境中有不同的表现和处理机制。 注意:由于 async 函数本质上返回 Promise,未捕获的 async 函数异常同样会触发这些事件。
javascript
// ❌ 未处理的 rejection - 危险!
Promise.reject(new Error('未处理的异常'))
// 浏览器: 触发 unhandledrejection 事件
// Node.js: 进程可能退出(取决于版本)
// async 函数未处理的异常也会触发 unhandledrejection
async function dangerousAsync() {
throw new Error('async 中的错误')
}
dangerousAsync() // ❌ 未 await 也未 catch - 触发 unhandledrejection
// ✅ 全局捕获未处理的 rejection
window.addEventListener('unhandledrejection', (event) => {
console.error('未捕获的 Promise rejection:', event.reason)
event.preventDefault() // 阻止默认行为(控制台警告)
})
// Promise 被延迟处理时触发
window.addEventListener('rejectionhandled', (event) => {
console.log('Promise rejection 被延迟处理')
})
// 示例:延迟处理 rejection
const promise = Promise.reject(new Error('延迟处理'))
// 立即触发 unhandledrejection
setTimeout(() => {
promise.catch(e => console.error(e))
// 触发 rejectionhandled
}, 1000)
事件监听器中的异常处理
DOM 事件监听器是异步执行的回调函数,其内部抛出的异常不会向外传播,必须在监听器内部显式处理,这是一个常见的陷阱。
javascript
// ❌ 外部 try-catch 无法捕获
try {
button.addEventListener('click', () => {
throw new Error('点击事件错误') // 不会被外部 catch 捕获
})
} catch (e) {
// 永远不会执行
}
// ✅ 在监听器内部处理
button.addEventListener('click', async (event) => {
try {
await handleClick(event)
} catch (error) {
console.error('处理点击事件失败:', error)
showErrorToast(error.message)
}
})
// ✅ 封装高阶函数统一处理
function withErrorHandling(handler) {
return async function(...args) {
try {
await handler(...args)
} catch (error) {
console.error('事件处理失败:', error)
reportError(error)
}
}
}
// 使用
button.addEventListener('click', withErrorHandling(async (event) => {
const data = await fetchData()
updateUI(data)
}))
异常处理的最佳实践
1. 错误边界 (React Error Boundary)
React 提供了错误边界(Error Boundary)机制来捕获组件树中的渲染错误,防止整个应用崩溃。错误边界是一个类组件,通过实现特定的生命周期方法来捕获子组件树中的 JavaScript 错误。
javascript
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
componentDidCatch(error, errorInfo) {
// 上报错误到监控系统
console.error('组件错误:', error)
console.error('错误栈:', errorInfo.componentStack)
reportError(error, { componentStack: errorInfo.componentStack })
}
render() {
if (this.state.hasError) {
return (
<div className="error-container">
<h1>出错了</h1>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
重试
</button>
</div>
)
}
return this.props.children
}
}
// 使用
function App() {
return (
<ErrorBoundary>
<UserProfile />
<PostList />
</ErrorBoundary>
)
}
重要限制: 错误边界无法捕获以下场景的错误:
- 事件处理器中的错误(需在事件处理函数内部 try-catch)
- 异步代码(setTimeout、Promise、async/await)
- 服务端渲染(SSR)中的错误
- 错误边界组件自身抛出的错误
2. 全局异常捕获
作为最后的防线,全局异常处理可以捕获所有未被处理的错误,避免应用完全崩溃,同时为错误监控提供数据来源。
javascript
// 浏览器环境
window.addEventListener('error', (event) => {
console.error('全局错误:', event.error)
// 上报错误
reportError({
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error,
})
event.preventDefault() // 阻止控制台打印
})
// 捕获未处理的 Promise rejection
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise rejection:', event.reason)
reportError({
type: 'unhandled-rejection',
reason: event.reason,
})
event.preventDefault()
})
// Node.js 环境
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error)
// 记录日志后退出进程
process.exit(1)
})
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise rejection:', reason)
// 记录日志,但不退出进程
})
3. 错误日志与监控
生产环境中的错误监控是发现和解决问题的关键。通过统一的错误上报机制,可以及时发现线上问题并收集足够的上下文信息用于排查。
javascript
// 统一的错误上报函数
function reportError(error, context = {}) {
// 构造错误信息
const errorInfo = {
message: error.message,
stack: error.stack,
name: error.name,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
...context,
}
// 发送到监控系统(Sentry、LogRocket 等)
if (window.Sentry) {
Sentry.captureException(error, { extra: context })
}
// 发送到自己的日志服务
fetch('/api/log-error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorInfo),
}).catch(err => {
// 避免错误上报本身出错
console.error('错误上报失败:', err)
})
}
// 包装 fetch 添加错误追踪
async function trackedFetch(url, options = {}) {
const startTime = Date.now()
try {
const response = await fetch(url, options)
if (!response.ok) {
const error = new NetworkError(
response.status,
`HTTP ${response.status}: ${response.statusText}`
)
error.url = url
error.duration = Date.now() - startTime
throw error
}
return response
} catch (error) {
// 添加请求上下文
error.url = url
error.method = options.method || 'GET'
error.duration = Date.now() - startTime
reportError(error)
throw error
}
}
异常处理的性能考虑
虽然异常处理是必需的,但也需要注意其性能影响。try-catch 语句本身开销很小,但频繁抛出和捕获异常会带来明显的性能损耗。
javascript
// try-catch 本身性能损耗很小
console.time('try-catch')
for (let i = 0; i < 1000000; i++) {
try {
const x = i * 2
} catch (e) {
// 不抛出异常时,性能影响可忽略
}
}
console.timeEnd('try-catch') // ~3ms
// 但抛出和捕获异常的开销较大
console.time('throw-catch')
for (let i = 0; i < 10000; i++) {
try {
throw new Error('test')
} catch (e) {
// 每次抛出异常都有明显开销
}
}
console.timeEnd('throw-catch') // ~26ms
性能优化建议:
- 避免用异常控制流程: 异常应该用于异常情况(exceptional cases),而非正常的控制流逻辑
- 减少异常创建频率: 避免在热点路径(如循环体)中频繁创建和抛出异常
- 考虑延迟栈追踪: 对于高频错误,可以考虑延迟生成堆栈信息以降低开销
- 使用错误码模式: 对于预期内的错误(如表单验证失败),可以返回错误码而非抛出异常
javascript
// ❌ 用异常控制流程(性能差)
function find(arr, predicate) {
try {
for (const item of arr) {
if (predicate(item)) {
throw item // 找到就抛出 - 不推荐!
}
}
} catch (result) {
return result
}
return null
}
// ✅ 正常的控制流程
function find(arr, predicate) {
for (const item of arr) {
if (predicate(item)) {
return item
}
}
return null
}
异常处理的核心原则
- 早发现,早处理: 在最接近异常发生的地方进行处理,便于定位和恢复
- 不要吞掉错误: 捕获异常后至少要记录日志,必要时重新抛出,避免静默失败
- 提供充分上下文: 自定义错误应包含足够的调试信息,如操作参数、状态快照等
- 区分错误类型: 使用自定义错误类或错误码区分业务错误、系统错误、第三方错误
- 用户体验优先: 向用户展示友好的错误提示,隐藏技术细节,提供可操作的建议
- 构建可观测性: 集成 Sentry、LogRocket 等监控工具,建立完善的错误上报和告警机制
- 防御性编程: 验证输入参数,使用可选链(?.)、空值合并(??),提前规避潜在异常