JavaScript 异常处理

在现代 JavaScript 开发中,合理的异常处理是构建健壮应用的基石。本文将探讨异常的本质特征、标准错误类型、捕获机制以及异步场景下的处理策略,帮助你建立完整的异常处理知识体系。

异常的本质

异常(Exception)是程序执行过程中发生的意外情况,它会打断正常的执行流程。理解异常的核心特征对于编写健壮的代码至关重要:

  1. 中断执行流(Interrupting Flow): 异常发生时,代码会立即停止执行当前语句及后续代码
  2. 向上传播(Propagation): 异常会沿着函数调用栈逐层向上传播,直到被捕获或到达顶层导致程序崩溃
  3. 包含上下文(Context): 异常对象携带丰富的调试信息,包括错误消息、堆栈跟踪、错误类型等
  4. 可恢复性(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

性能优化建议:

  1. 避免用异常控制流程: 异常应该用于异常情况(exceptional cases),而非正常的控制流逻辑
  2. 减少异常创建频率: 避免在热点路径(如循环体)中频繁创建和抛出异常
  3. 考虑延迟栈追踪: 对于高频错误,可以考虑延迟生成堆栈信息以降低开销
  4. 使用错误码模式: 对于预期内的错误(如表单验证失败),可以返回错误码而非抛出异常
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
}

异常处理的核心原则

  1. 早发现,早处理: 在最接近异常发生的地方进行处理,便于定位和恢复
  2. 不要吞掉错误: 捕获异常后至少要记录日志,必要时重新抛出,避免静默失败
  3. 提供充分上下文: 自定义错误应包含足够的调试信息,如操作参数、状态快照等
  4. 区分错误类型: 使用自定义错误类或错误码区分业务错误、系统错误、第三方错误
  5. 用户体验优先: 向用户展示友好的错误提示,隐藏技术细节,提供可操作的建议
  6. 构建可观测性: 集成 Sentry、LogRocket 等监控工具,建立完善的错误上报和告警机制
  7. 防御性编程: 验证输入参数,使用可选链(?.)、空值合并(??),提前规避潜在异常
相关推荐
小岛前端4 小时前
🔥Vue3 移动端组件精选!满足各种场景!
前端·vue.js·微信小程序
用户1510581047434 小时前
带leading和trailing的防抖和节流
前端
IT小哥哥呀4 小时前
论文见解:REACT:在语言模型中协同推理和行动
前端·人工智能·react.js·语言模型
一枚前端小能手4 小时前
🚫 请求取消还在用flag?AbortController让你的异步操作更优雅
前端·javascript
code_YuJun4 小时前
前端脚手架开发流程
前端
golang学习记4 小时前
从0死磕全栈之使用 VS Code 调试 Next.js 应用完整指南
前端
Mintopia4 小时前
🧩 隐私计算技术在 Web AIGC 数据处理中的应用实践
前端·javascript·aigc
尘世中一位迷途小书童4 小时前
代码质量保障:ESLint + Prettier + Stylelint 三剑客完美配置
前端·架构
Mintopia4 小时前
🧭 Next.js 架构与运维:当现代前端拥有了“分布式的灵魂”
前端·javascript·全栈