JavaScript 进阶 九:错误处理与异常捕获
在 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 数据
HTML
<div id="result"></div>
<button onclick="parseJSON()">解析 JSON</button>
JavaScript
function parseJSON() {
const jsonString = '{"name": "Alice", "age": 25}' // 有效的 JSON
// const jsonString = '{"name": Alice}'; // 无效的 JSON
try {
const user = JSON.parse(jsonString)
document.getElementById('result').innerHTML
= `姓名: ${user.name}, 年龄: ${user.age}`
}
catch (error) {
document.getElementById('result').innerHTML
= `JSON 解析失败: ${error.message}`
}
}
try...catch 的限制
重要限制
- 仅对运行时错误有效:语法错误无法捕获
- 同步工作:无法直接捕获异步操作中的错误
异步错误无法直接捕获
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')
总结
- 始终处理可预见的错误:不要忽略你知道可能发生的错误
- 使用适当的错误类型:创建有意义的自定义错误类
- 异步错误特殊处理:Promise 和 async/await 需要专门的错误处理
- 合理使用 finally:确保资源清理和状态重置
- 实现全局错误处理:捕获未处理的错误并提供用户友好的反馈
通过掌握这些错误处理技术,您将能够构建更加健壮、可靠的 JavaScript 应用程序。记住,好的错误处理不仅能防止应用崩溃,还能提供更好的用户体验和更快的故障排查。