Skip to content

JavaScript 进阶 九:错误处理与异常捕获

约 1971 字大约 7 分钟

javascript

2020-02-09

在 JavaScript 开发中,错误处理是构建健壮应用的关键技能。本文将深入探讨 JavaScript 错误处理机制,从基础语法到高级实践,帮助您写出更可靠的代码。

为什么需要错误处理?

错误不可避免

在真实的软件环境中,错误是不可避免的。好的代码不仅在一切顺利时能够正常工作,而且在出现问题时也能保持可预测性和安全性。

JavaScript 中的错误主要分为几类:

  • 语法错误:代码编写不规范,在解析阶段就会报错
  • 运行时错误:代码执行过程中出现的错误
  • 逻辑错误:代码逻辑有误,但不会抛出异常
常见错误示例
// 1. 引用错误
console.log(undefinedVariable) // ReferenceError

// 2. 类型错误
null.function() // TypeError

// 3. 语法错误(无法通过 try...catch 捕获)
// console.log("hello; // SyntaxError

Error 对象详解

当错误发生时,JavaScript 会创建一个 Error 对象,包含以下重要属性:

try {
  let user = undefinedUser
}
catch (error) {
  console.log('错误名称:', error.name) // ReferenceError
  console.log('错误信息:', error.message) // undefinedUser is not defined
  console.log('调用栈:', error.stack) // 详细的调用栈信息
}

Error 对象属性

  • name:错误类型名称
  • message:错误描述信息
  • stack:调用栈信息(调试用)

try...catch 基础

基本语法

try {
  // 可能出错的代码
  riskyOperation()
}
catch (error) {
  // 错误处理逻辑
  console.error('操作失败:', error.message)
}

实际应用示例

JSON 解析错误处理

处理可能格式错误的 JSON 数据

try...catch 的限制

重要限制

  1. 仅对运行时错误有效:语法错误无法捕获
  2. 同步工作:无法直接捕获异步操作中的错误
异步错误无法直接捕获
try {
  setTimeout(() => {
    throw new Error('异步错误') // 这个错误无法被外部的 catch 捕获
  }, 1000)
}
catch (error) {
  console.log('这行代码不会执行')
}

throw 操作符与自定义错误

基本用法

throw 语句允许您创建自定义错误:

function validateAge(age) {
  if (age < 0) {
    throw new Error('年龄不能为负数')
  }
  if (age > 150) {
    throw new Error('年龄超出合理范围')
  }
  return true
}

try {
  validateAge(-5)
}
catch (error) {
  console.error('验证失败:', error.message)
}

自定义错误类型

通过继承 Error 类创建更具体的错误类型:

自定义验证错误
class ValidationError extends Error {
  constructor(field, message) {
    super(`${field} 验证失败: ${message}`)
    this.name = 'ValidationError'
    this.field = field
    this.timestamp = new Date().toISOString()
  }
}

class NetworkError extends Error {
  constructor(url, status) {
    super(`请求 ${url} 失败,状态码: ${status}`)
    this.name = 'NetworkError'
    this.status = status
  }
}

// 使用示例
function registerUser(userData) {
  if (!userData.username) {
    throw new ValidationError('username', '用户名不能为空')
  }
  if (userData.password.length < 8) {
    throw new ValidationError('password', '密码至少8位')
  }

  // 模拟网络请求
  throw new NetworkError('/api/register', 500)
}

try {
  registerUser({ username: 'john', password: '123' })
}
catch (error) {
  if (error instanceof ValidationError) {
    console.error('输入验证错误:', error.message)
  }
  else if (error instanceof NetworkError) {
    console.error('网络错误:', error.message)
  }
  else {
    console.error('未知错误:', error.message)
  }
}

finally 块的作用

finally 块中的代码总是会执行,无论是否发生错误:

function processFile(filename) {
  let fileHandle = null

  try {
    console.log(`开始处理文件: ${filename}`)
    fileHandle = `handle_${filename}` // 模拟文件打开
    // 模拟文件处理可能出错
    if (Math.random() > 0.5) {
      throw new Error('文件处理失败')
    }
    console.log('文件处理成功')
  }
  catch (error) {
    console.error('处理过程中出错:', error.message)
    throw error // 重新抛出错误
  }
  finally {
    // 无论成功还是失败,都要关闭文件
    console.log(`关闭文件句柄: ${fileHandle}`)
    fileHandle = null
  }
}

try {
  processFile('data.txt')
}
catch (error) {
  console.log('外部捕获:', error.message)
}

异步错误处理

Promise 错误处理

// .catch() 方法
fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('请求失败:', error.message))

// 或者使用 async/await
async function fetchData() {
  try {
    const response = await fetch('/api/data')
    if (!response.ok) {
      throw new Error(`HTTP错误! 状态码: ${response.status}`)
    }
    const data = await response.json()
    return data
  }
  catch (error) {
    console.error('获取数据失败:', error.message)
    throw error // 可以选择重新抛出
  }
}

异步函数中的错误处理模式

健壮的异步错误处理
class ApiService {
  async request(url, options = {}) {
    try {
      const response = await fetch(url, {
        timeout: 5000,
        ...options
      })

      if (!response.ok) {
        throw new Error(`请求失败: ${response.status}`)
      }

      return await response.json()
    }
    catch (error) {
      // 记录错误日志
      console.error(`API请求错误 [${url}]:`, error.message)

      // 根据错误类型提供友好的错误信息
      if (error.name === 'TypeError' && error.message.includes('fetch')) {
        throw new Error('网络连接失败,请检查网络设置')
      }

      throw error
    }
  }
}

// 使用示例
const api = new ApiService()

async function loadUserData(userId) {
  try {
    const user = await api.request(`/api/users/${userId}`)
    const posts = await api.request(`/api/users/${userId}/posts`)

    return { user, posts }
  }
  catch (error) {
    console.error('加载用户数据失败:', error.message)
    // 可以在这里显示用户友好的错误信息
    return null
  }
}

最佳实践与设计模式

1. 错误传播与重新抛出

重新抛出原则

只处理你能处理的错误,将其他错误传递给上层调用者。

function processUserData(userData) {
  try {
    // 数据验证
    if (!userData.email.includes('@')) {
      throw new ValidationError('email', '邮箱格式不正确')
    }

    // 业务逻辑处理
    const result = complexBusinessLogic(userData)
    return result
  }
  catch (error) {
    // 只处理验证错误,其他错误重新抛出
    if (error instanceof ValidationError) {
      console.warn('输入验证警告:', error.message)
      return { success: false, error: error.message }
    }

    // 重新抛出未知错误
    throw error
  }
}

// 上层调用
try {
  const result = processUserData({ email: 'invalid-email' })
  console.log(result)
}
catch (error) {
  // 这里会捕获到除 ValidationError 外的所有错误
  console.error('严重错误:', error.message)
}

2. 全局错误处理

// 全局错误捕获
window.addEventListener('error', (event) => {
  console.error('全局错误:', event.error)
  // 可以在这里发送错误报告到服务器
  sendErrorReport(event.error)
})

// 未处理的 Promise 拒绝
window.addEventListener('unhandledrejection', (event) => {
  console.error('未处理的 Promise 拒绝:', event.reason)
  event.preventDefault() // 防止默认的错误输出
})

// 错误报告函数
async function sendErrorReport(error) {
  const report = {
    message: error.message,
    stack: error.stack,
    url: window.location.href,
    userAgent: navigator.userAgent,
    timestamp: new Date().toISOString()
  }

  try {
    await fetch('/api/error-report', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(report)
    })
  }
  catch (reportError) {
    console.warn('错误报告发送失败:', reportError.message)
  }
}

3. 防御性编程模式

防御性函数设计
class DataProcessor {
  // 带有完整错误处理的处理函数
  async processWithRetry(operation, maxRetries = 3) {
    let lastError

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        console.log(`尝试第 ${attempt} 次执行...`)
        const result = await operation()
        console.log('操作成功')
        return result
      }
      catch (error) {
        lastError = error
        console.warn(`${attempt} 次尝试失败:`, error.message)

        if (attempt < maxRetries) {
          // 指数退避
          const delay = Math.min(1000 * 2 ** (attempt - 1), 30000)
          console.log(`等待 ${delay}ms 后重试...`)
          await new Promise(resolve => setTimeout(resolve, delay))
        }
      }
    }

    throw new Error(`操作在 ${maxRetries} 次尝试后失败,最后错误: ${lastError.message}`)
  }

  // 安全的数据访问
  getSafe(obj, path, defaultValue = null) {
    try {
      const value = path.split('.').reduce((current, key) => {
        return current && current[key] !== undefined ? current[key] : undefined
      }, obj)

      return value !== undefined ? value : defaultValue
    }
    catch (error) {
      console.warn(`安全访问路径 ${path} 失败:`, error.message)
      return defaultValue
    }
  }
}

// 使用示例
const processor = new DataProcessor()

// 带重试的操作
processor.processWithRetry(async () => {
  const response = await fetch('/api/unstable-endpoint')
  if (!response.ok)
    throw new Error(`HTTP ${response.status}`)
  return response.json()
}).then(data => console.log('最终结果:', data)).catch(error => console.error('所有重试都失败了:', error.message))

// 安全数据访问
const user = { profile: { address: { city: '北京' } } }
const city = processor.getSafe(user, 'profile.address.city', '未知')
const zipCode = processor.getSafe(user, 'profile.address.zipCode', '000000')

总结

  1. 始终处理可预见的错误:不要忽略你知道可能发生的错误
  2. 使用适当的错误类型:创建有意义的自定义错误类
  3. 异步错误特殊处理:Promise 和 async/await 需要专门的错误处理
  4. 合理使用 finally:确保资源清理和状态重置
  5. 实现全局错误处理:捕获未处理的错误并提供用户友好的反馈

通过掌握这些错误处理技术,您将能够构建更加健壮、可靠的 JavaScript 应用程序。记住,好的错误处理不仅能防止应用崩溃,还能提供更好的用户体验和更快的故障排查。

参考