Day 9: 错误处理
🎯 学习目标
- 理解 错误处理 的核心概念和工作原理
- 掌握相关语法和最佳实践
- 学会在实际项目中应用 错误处理
💡 核心概念
错误处理是编写健壮程序的关键。JavaScript提供了多种错误处理机制,包括try-catch、throw、Error对象等,帮助我们优雅地处理和调试错误。
关键概念
1. 错误类型
JavaScript有以下常见错误类型:
- Error:基础错误类型
- SyntaxError:语法错误
eval('1 + * 2'); // SyntaxError
- ReferenceError:引用不存在的变量
console.log(notDefined); // ReferenceError
- TypeError:类型错误
null.toString(); // TypeError
- RangeError:值超出有效范围
new Array(-1); // RangeError
- URIError:URI相关错误
decodeURIComponent('%'); // URIError
2. try-catch-finally
JavaScript的错误处理语法:
try {
// 可能出错的代码
riskyOperation();
} catch (error) {
// 捕获错误后的处理
console.error('发生错误:', error.message);
} finally {
// 无论是否出错都会执行
cleanup();
}
重要说明:
- catch块可以省略(配合finally使用)
- finally块总是执行(即使catch中有return)
- try块中抛出的错误会被catch捕获
3. throw语句
主动抛出错误:
// 抛出Error对象
throw new Error('自定义错误');
// 抛出特定类型错误
throw new TypeError('类型错误');
throw new ReferenceError('引用错误');
// 抛出任意值(不推荐)
throw '简单错误字符串';
throw 404;
throw {code: 500, message: '服务器错误'};
自定义错误:
class CustomError extends Error {
constructor(message) {
super(message);
this.name = 'CustomError';
}
}
throw new CustomError('自定义错误');
📝 核心API和语法
Error对象
new Error([message[, fileName[, lineNumber]]])
功能: 创建错误对象
参数:
- message: 错误描述信息
- fileName: 文件名(非标准)
- lineNumber: 行号(非标准)
属性:
- message: 错误信息
- name: 错误名称
- stack: 错误堆栈(非标准但广泛支持)
🎮 示例代码
示例1:基本错误处理
function divide(a, b) {
try {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('参数必须是数字');
}
if (b === 0) {
throw new Error('除数不能为零');
}
return a / b;
} catch (error) {
console.error('错误类型:', error.name);
console.error('错误信息:', error.message);
console.error('错误堆栈:', error.stack);
return null; // 返回默认值
} finally {
console.log('除法操作完成');
}
}
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // null(捕获错误)
console.log(divide('10', 2)); // null(捕获错误)
/*
输出:
除法操作完成
5
错误类型: Error
错误信息: 除数不能为零
除法操作完成
null
错误类型: TypeError
错误信息: 参数必须是数字
除法操作完成
null
*/
示例2:异步错误处理
// Promise错误处理
fetchData()
.then(data => {
console.log('数据:', data);
})
.catch(error => {
console.error('请求失败:', error);
});
// async/await错误处理
async function getData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('获取数据失败:', error);
// 可以重新抛出或返回默认值
return null;
}
}
// 事件错误处理
element.addEventListener('click', async function() {
try {
await riskyOperation();
} catch (error) {
console.error('操作失败:', error);
// 显示错误提示给用户
showErrorToast(error.message);
}
});
示例3:自定义错误类
// 自定义验证错误
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
// 自定义API错误
class APIError extends Error {
constructor(statusCode, message) {
super(message);
this.name = 'APIError';
this.statusCode = statusCode;
}
}
// 使用自定义错误
function validateEmail(email) {
if (!email || !email.includes('@')) {
throw new ValidationError('email', '无效的邮箱地址');
}
}
function fetchUserData(userId) {
if (!userId) {
throw new APIError(400, '缺少用户ID');
}
// 模拟API调用
if (userId === '404') {
throw new APIError(404, '用户不存在');
}
return {id: userId, name: '张三'};
}
// 使用
try {
validateEmail('invalid-email');
} catch (error) {
if (error instanceof ValidationError) {
console.error(`字段 ${error.field} 验证失败: ${error.message}`);
}
}
try {
const user = fetchUserData('404');
} catch (error) {
if (error instanceof APIError) {
console.error(`API错误 ${error.statusCode}: ${error.message}`);
// 根据状态码处理
if (error.statusCode === 404) {
showUserNotFoundPage();
}
}
}
示例4:错误边界(Error Boundary)
// 错误处理包装器
function safeExecute(fn, errorHandler) {
return function(...args) {
try {
return fn.apply(this, args);
} catch (error) {
console.error('执行出错:', error);
if (errorHandler) {
return errorHandler(error);
}
}
};
}
// 使用
function riskyOperation(x, y) {
if (y === 0) throw new Error('除数不能为零');
return x / y;
}
const safeDivide = safeExecute(riskyOperation, function(error) {
console.error('除法失败:', error.message);
return 0; // 默认值
});
console.log(safeDivide(10, 2)); // 5
console.log(safeDivide(10, 0)); // 0(错误被捕获)
// 异步版本
async function safeAsync(fn, errorHandler) {
return async function(...args) {
try {
return await fn.apply(this, args);
} catch (error) {
console.error('异步操作出错:', error);
if (errorHandler) {
return errorHandler(error);
}
}
};
}
const safeFetch = safeAsync(fetchData, function(error) {
console.error('获取数据失败');
return []; // 返回空数组
});
const data = await safeFetch();
✍️ 练习任务
练习 1: 练习1:实现输入验证
创建一个表单验证函数,要求:
- 验证用户对象
function validateUser(user) {
// 验证规则:
// - name: 必填,2-20字符
// - email: 必填,有效邮箱格式
// - age: 可选,18-120之间
// - 返回:{valid: boolean, errors: string[]}
}
-
抛出ValidationError
-
使用try-catch处理
测试用例:
try {
validateUser({name: 'A', email: 'invalid'});
} catch (error) {
console.log(error.errors);
// ['name长度必须2-20字符', '邮箱格式无效']
}
提示: 检查每个字段,收集所有错误后一起抛出
练习 2: 练习2:重试机制
实现一个带重试的异步函数:
async function retry(fn, maxRetries = 3, delay = 1000) {
// 实现重试逻辑:
// 1. 尝试执行fn()
// 2. 失败后等待delay毫秒
// 3. 重试,最多maxRetries次
// 4. 全部失败后抛出最后一个错误
}
使用示例:
let attempts = 0;
async function unstableFunction() {
attempts++;
if (attempts < 3) {
throw new Error('暂时失败');
}
return '成功';
}
const result = await retry(unstableFunction, 5, 500);
console.log(result); // '成功'(重试2次后成功)
提示: 使用for循环或递归,配合await sleep()
练习 3: 练习3:错误日志系统
实现一个错误日志系统:
class ErrorLogger {
// 记录错误
log(error, context = {}) {}
// 获取所有错误
getErrors() {}
// 按类型筛选
getErrorsByType(type) {}
// 清空日志
clear() {}
// 导出为JSON
export() {}
}
功能要求:
- 记录错误时间、类型、消息、堆栈
- 支持添加上下文信息(用户、操作等)
- 统计各类错误数量
- 导出格式化的错误报告
测试:
const logger = new ErrorLogger();
try {
riskyOperation();
} catch (error) {
logger.log(error, {user: '张三', action: '保存数据'});
}
console.log(logger.export());
提示: 使用数组存储错误对象,每个错误包含timestamp、error、context
🎓 今日挑战
今日挑战:实现完整的API客户端
创建一个功能完善的API客户端类:
功能要求:
- 请求拦截
class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.interceptors = [];
}
// 添加拦截器
useInterceptor(fn) {
this.interceptors.push(fn);
}
async request(url, options = {}) {
// 执行所有拦截器
// 发送请求
// 处理响应和错误
}
}
- 错误处理
- 网络错误:重试3次
- 4xx错误:抛出ClientError
- 5xx错误:抛出ServerError
- 超时错误:抛出TimeoutError
- 响应拦截
- 自动解析JSON
- 检查响应状态
- 提取数据
- 便捷方法
client.get(url, params)
client.post(url, data)
client.put(url, data)
client.delete(url)
- 使用示例
const api = new APIClient('https://api.example.com');
// 添加认证拦截器
api.useInterceptor((options) => {
options.headers = {
...options.headers,
'Authorization': `Bearer ${getToken()}`
};
return options;
});
// 使用
try {
const user = await api.get('/user/123');
console.log(user);
} catch (error) {
if (error instanceof ClientError) {
console.error('客户端错误:', error.message);
} else if (error instanceof ServerError) {
console.error('服务器错误:', error.message);
}
}
扩展功能(可选):
- 请求缓存
- 请求取消
- 上传/下载进度
- 请求队列(并发限制)
提示: 使用fetch API,配合try-catch和自定义错误类
💡 常见问题
Q1: 什么时候应该使用try-catch?
A: 应该使用的场景:
1. 可能失败的操作:
try {
const data = JSON.parse(userInput);
} catch (e) {
console.error('JSON解析失败');
}
2. 外部资源访问:
try {
const response = await fetch(url);
} catch (e) {
console.error('网络请求失败');
}
3. 类型转换:
try {
const num = parseInt(userInput);
} catch (e) {
// 处理转换错误
}
不应该使用的场景:
- 控制流程(不要用错误处理代替if)
- 可预见的错误(应该用if检查)
// ❌ 不好
try {
if (obj.prop) {
// ...
}
} catch (e) {}
// ✅ 好
if (obj && obj.prop) {
// ...
}
Q2: 如何调试未捕获的错误?
A: 方法1:全局错误处理:
// 捕获未处理的错误
window.addEventListener('error', function(event) {
console.error('全局错误:', event.error);
// 发送到日志服务器
logError(event.error);
});
// 捕获未处理的Promise拒绝
window.addEventListener('unhandledrejection', function(event) {
console.error('未处理的Promise:', event.reason);
event.preventDefault(); // 阻止默认处理
});
方法2:source map:
- 生产环境启用source map
- 错误堆栈显示原始代码位置
方法3:错误监控服务:
- Sentry
- Rollbar
- Bugsnag
方法4:开发者工具:
- Chrome DevTools的Console面板
- Break on exceptions(暂停在异常处)
- Stack trace分析
Q3: Promise错误和try-catch有什么区别?
A: 关键区别:
1. try-catch不能捕获异步错误:
try {
setTimeout(() => {
throw new Error('无法捕获');
}, 100);
} catch (e) {
// 不会执行
}
// 解决:使用Promise
catch Promise.reject('可以捕获');
2. Promise错误的传播:
Promise.reject('错误')
.then(() => console.log('不执行'))
.then(() => console.log('不执行'))
.catch(err => console.log('捕获:', err));
// 错误会一直传播直到被捕获
3. async/await统一了错误处理:
async function foo() {
try {
await Promise.reject('错误');
} catch (e) {
console.log('捕获:', e);
}
}
最佳实践:
- 同步代码:try-catch
- 异步代码:Promise.catch()或async/await
- 全局:unhandledrejection事件
Q4: 如何创建有意义的错误信息?
A: 好的错误信息应该:
1. 清晰具体:
// ❌ 不好
throw new Error('错误');
// ✅ 好
throw new Error('用户ID不能为空');
2. 包含上下文:
function updateUser(userId, data) {
if (!userId) {
throw new Error(
`updateUser失败: userId为空 (data: ${JSON.stringify(data)})`
);
}
}
3. 提供解决建议:
class ValidationError extends Error {
constructor(field, value, rule) {
super(
`字段${field}验证失败: ${value} 不符合规则${rule}. ` +
`请检查${field}的值是否正确。`
);
this.field = field;
this.value = value;
}
}
4. 使用错误代码:
const ERROR_CODES = {
USER_NOT_FOUND: 'USER_NOT_FOUND',
INVALID_PASSWORD: 'INVALID_PASSWORD',
NETWORK_ERROR: 'NETWORK_ERROR'
};
throw new Error(ERROR_CODES.USER_NOT_FOUND);
5. 结构化错误:
// 返回错误对象而不是throw
function safeOperation() {
return {
success: false,
error: {
code: 'INVALID_INPUT',
message: '输入无效',
details: {field: 'email', value: 'bad'}
}
};
}
学习时间: 2026-03-09 22:22
难度: ⭐⭐⭐⭐☆
预计用时: 2-3 小时
关键词: 错误处理