为什么需要错误处理?
良好的错误处理可以:
- 防止程序崩溃
- 提供有意义的错误信息
- 增强用户体验
- 便于调试和维护
- 提高代码的健壮性
- 帮助监控应用健康状况
错误处理的重要性
在Web应用中,错误处理不仅仅是技术问题,更是用户体验的重要组成部分。恰当的错误处理可以:
- 避免白屏或不可用状态
- 提供清晰的恢复路径
- 建立用户信任
- 减少用户流失
基本的错误处理:try...catch
try...catch语句是JavaScript中最基本的错误处理机制。
try...catch基本语法
try {
// 可能出错的代码
const result = riskyOperation();
console.log('结果:', result);
} catch (error) {
// 错误处理
console.error('发生错误:', error.message);
} finally {
// 无论是否出错都会执行
console.log('操作完成');
}
// 示例:处理JSON解析错误
function parseJSONSafely(jsonString) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.error('JSON解析失败:', error.message);
return null;
}
}
const validJSON = '{"name": "张三", "age": 25}';
const invalidJSON = '{name: 张三}'; // 缺少引号
console.log(parseJSONSafely(validJSON)); // {name: "张三", age: 25}
console.log(parseJSONSafely(invalidJSON)); // null
// 嵌套的try-catch
function complexOperation() {
try {
// 外层操作
const data = fetchData();
try {
// 内层操作
const processed = processData(data);
return processed;
} catch (innerError) {
console.error('数据处理失败:', innerError.message);
return null;
}
} catch (outerError) {
console.error('数据获取失败:', outerError.message);
throw new Error('操作完全失败');
}
}
// 条件catch块
try {
riskyOperation();
} catch (error) {
if (error instanceof TypeError) {
// 处理类型错误
handleTypeError(error);
} else if (error instanceof RangeError) {
// 处理范围错误
handleRangeError(error);
} else {
// 处理其他错误
handleGenericError(error);
}
}
最佳实践: 在catch块中应该处理错误或重新抛出错误,不要静默忽略错误。finally块适合用于清理资源,如关闭文件、清除定时器等。
错误类型
JavaScript内置了多种错误类型,了解这些类型有助于编写更有针对性的错误处理代码。
| 错误类型 | 描述 | 触发场景 | 示例 |
|---|---|---|---|
| Error | 通用错误类型 | 自定义错误的基础 | new Error('消息') |
| SyntaxError | 语法错误 | 代码解析错误 | JSON.parse('invalid') |
| TypeError | 类型错误 | 操作不适合的类型 | null.toString() |
| ReferenceError | 引用错误 | 引用未定义的变量 | console.log(undefinedVar) |
| RangeError | 范围错误 | 数值超出有效范围 | new Array(-1) |
| URIError | URI处理错误 | URI编码/解码错误 | decodeURI('%') |
| EvalError | eval函数错误 | eval()使用不当 | eval('x =') |
| AggregateError | 聚合错误 | 多个错误的集合 | Promise.any()全部失败 |
处理特定错误类型
function handleOperation() {
try {
// 可能抛出不同类型错误的代码
riskyOperation();
} catch (error) {
if (error instanceof TypeError) {
console.error('类型错误:', error.message);
// 处理类型错误
} else if (error instanceof RangeError) {
console.error('范围错误:', error.message);
// 处理范围错误
} else if (error instanceof SyntaxError) {
console.error('语法错误:', error.message);
// 处理语法错误
} else {
console.error('未知错误:', error.message);
// 处理其他错误
}
}
}
// 现代写法:使用switch和constructor
try {
riskyOperation();
} catch (error) {
switch (error.constructor) {
case TypeError:
console.log('处理类型错误');
break;
case ReferenceError:
console.log('处理引用错误');
break;
case RangeError:
console.log('处理范围错误');
break;
default:
console.log('处理其他错误');
}
}
// 获取错误详细信息
function analyzeError(error) {
return {
name: error.name,
message: error.message,
stack: error.stack, // 调用栈(非标准但广泛支持)
fileName: error.fileName, // 文件名(非标准)
lineNumber: error.lineNumber, // 行号(非标准)
columnNumber: error.columnNumber // 列号(非标准)
};
}
try {
undefinedFunction();
} catch (error) {
const errorInfo = analyzeError(error);
console.log('错误分析:', errorInfo);
}
自定义错误
创建自定义错误类可以提供更具体的错误信息和更好的错误分类。
自定义错误类
// 基础自定义错误
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.timestamp = new Date().toISOString();
this.code = 'VALIDATION_ERROR';
}
toJSON() {
return {
name: this.name,
message: this.message,
field: this.field,
timestamp: this.timestamp,
code: this.code,
stack: this.stack
};
}
}
// 更具体的错误类
class EmailValidationError extends ValidationError {
constructor(email) {
super(`无效的邮箱地址: ${email}`, 'email');
this.name = 'EmailValidationError';
this.email = email;
this.code = 'INVALID_EMAIL';
}
}
class NetworkError extends Error {
constructor(message, url, statusCode) {
super(message);
this.name = 'NetworkError';
this.url = url;
this.statusCode = statusCode;
this.code = 'NETWORK_ERROR';
this.timestamp = new Date().toISOString();
}
}
// 使用自定义错误
function validateUser(user) {
const errors = [];
if (!user.email || !user.email.includes('@')) {
errors.push(new EmailValidationError(user.email));
}
if (!user.age || user.age < 0) {
errors.push(new ValidationError('年龄不能为负数', 'age'));
}
if (errors.length > 0) {
throw new AggregateError(errors, '用户数据验证失败');
}
}
// 处理自定义错误
try {
validateUser({ email: 'invalid-email', age: -5 });
} catch (error) {
if (error instanceof AggregateError) {
console.error('多个验证错误:');
error.errors.forEach((validationError, index) => {
console.error(`${index + 1}. ${validationError.field}: ${validationError.message}`);
});
} else if (error instanceof ValidationError) {
console.error(`验证失败 - 字段: ${error.field}, 错误: ${error.message}`);
} else {
console.error('未知错误:', error);
}
}
// 错误工厂函数
class ErrorFactory {
static createValidationError(field, message) {
return new ValidationError(message, field);
}
static createNetworkError(url, statusCode, message = '网络请求失败') {
return new NetworkError(message, url, statusCode);
}
static createDatabaseError(operation, details) {
const error = new Error(`数据库操作失败: ${operation}`);
error.name = 'DatabaseError';
error.operation = operation;
error.details = details;
error.code = 'DATABASE_ERROR';
return error;
}
}
异步错误处理
处理Promise和async/await中的错误需要特别注意,因为它们的错误处理机制与同步代码有所不同。
异步错误处理
// Promise错误处理
function fetchData() {
return fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP错误! 状态: ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error('获取数据失败:', error.message);
throw error; // 重新抛出以便外部处理
});
}
// async/await错误处理
async function loadUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`用户 ${userId} 不存在`);
}
const user = await response.json();
return user;
} catch (error) {
console.error('加载用户数据失败:', error.message);
// 根据错误类型采取不同措施
if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) {
// 处理网络错误
showNetworkError();
} else if (error.message.includes('不存在')) {
// 处理用户不存在的情况
showUserNotFound();
} else {
// 处理其他错误
showGenericError();
}
throw error;
}
}
// 并行操作的错误处理
async function loadMultipleResources() {
try {
const [users, posts, settings] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/settings').then(r => r.json())
]);
return { users, posts, settings };
} catch (error) {
console.error('加载资源失败:', error);
// 即使部分失败,也可能返回部分数据
return { users: [], posts: [], settings: {} };
}
}
// 使用Promise.allSettled处理部分失败
async function loadWithPartialSuccess() {
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/settings')
]);
const data = {};
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
const key = ['users', 'posts', 'settings'][index];
data[key] = result.value;
} else {
console.error(`资源 ${index} 加载失败:`, result.reason);
}
});
return data;
}
// 全局Promise错误处理
window.addEventListener('unhandledrejection', event => {
console.error('未处理的Promise拒绝:', event.reason);
// 可以在这里报告错误到监控系统
reportErrorToService({
type: 'unhandledrejection',
reason: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
timestamp: new Date().toISOString()
});
event.preventDefault(); // 阻止默认错误输出
});
// 全局错误处理
window.addEventListener('error', event => {
console.error('全局错误:', event.error);
reportErrorToService({
type: 'error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error?.stack,
timestamp: new Date().toISOString()
});
// 可以选择阻止默认行为
// event.preventDefault();
});
错误处理最佳实践
错误处理原则:
- 尽早发现错误,尽早处理
- 提供有意义的错误信息
- 不要静默吞掉错误
- 在适当的地方处理错误
- 记录错误以便调试
- 为用户提供友好的错误信息
- 建立错误监控和报告机制
错误处理工具函数
// 错误处理工具类
class ErrorHandler {
static handle(error, context = '') {
const errorInfo = {
message: error.message,
name: error.name,
stack: error.stack,
context: context,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
};
// 记录错误
this.logError(errorInfo);
// 根据环境采取不同措施
if (process.env.NODE_ENV === 'development') {
// 开发环境:显示详细错误
console.error('开发环境错误:', errorInfo);
this.showDeveloperError(errorInfo);
} else {
// 生产环境:用户友好的错误信息
this.showUserFriendlyError(error);
// 发送错误报告
this.reportError(errorInfo);
}
}
static logError(errorInfo) {
// 这里可以集成错误日志服务
console.error('错误记录:', errorInfo);
// 也可以存储到localStorage或IndexedDB
try {
const existingErrors = JSON.parse(localStorage.getItem('appErrors') || '[]');
existingErrors.push(errorInfo);
localStorage.setItem('appErrors', JSON.stringify(existingErrors.slice(-50))); // 只保留最近50个
} catch (e) {
console.warn('无法保存错误到本地存储:', e);
}
}
static showUserFriendlyError(error) {
const message = this.getUserFriendlyMessage(error);
this.displayErrorToast(message);
}
static getUserFriendlyMessage(error) {
if (error instanceof NetworkError) {
return '网络连接失败,请检查网络设置';
} else if (error instanceof ValidationError) {
return `请输入有效的${error.field}信息`;
} else if (error.name === 'TypeError' && error.message.includes('fetch')) {
return '网络请求失败,请检查网络连接';
} else {
return '系统繁忙,请稍后重试';
}
}
static displayErrorToast(message) {
// 显示错误提示
const toast = document.createElement('div');
toast.textContent = message;
toast.style.cssText = `
position: fixed; top: 20px; right: 20px;
background: #f44336; color: white; padding: 12px 16px;
border-radius: 4px; z-index: 10000; max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 5000);
}
static showDeveloperError(errorInfo) {
// 在开发环境中显示详细错误
const devPanel = document.createElement('div');
devPanel.style.cssText = `
position: fixed; bottom: 0; left: 0; right: 0;
background: #ffebee; color: #c62828; padding: 10px;
border-top: 2px solid #c62828; z-index: 10000;
font-family: monospace; font-size: 12px; max-height: 200px;
overflow-y: auto;
`;
devPanel.innerHTML = `
开发错误信息:
上下文: ${errorInfo.context}
错误: ${errorInfo.message}
堆栈跟踪
${errorInfo.stack}
`;
document.body.appendChild(devPanel);
}
static reportError(errorInfo) {
// 发送错误报告到服务器
if (navigator.onLine) {
fetch('/api/error-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorInfo)
}).catch(e => console.warn('无法发送错误报告:', e));
}
}
// 包装函数,自动添加错误处理
static wrapAsync(fn, context = '') {
return async (...args) => {
try {
return await fn(...args);
} catch (error) {
this.handle(error, context);
throw error;
}
};
}
}
// 使用示例
try {
riskyOperation();
} catch (error) {
ErrorHandler.handle(error, '执行关键操作时');
}
// 包装异步函数
const safeFetch = ErrorHandler.wrapAsync(fetch, '网络请求');
safeFetch('/api/data')
.then(response => response.json())
.catch(error => {
// 错误已经被ErrorHandler处理
console.log('操作失败,但错误已处理');
});
防御性编程
防御性编程是一种预防错误发生的编程实践,通过预先检查来避免错误。
防御性编程技巧
// 1. 参数验证
function createUser(userData) {
// 验证必需参数
if (!userData || typeof userData !== 'object') {
throw new ValidationError('用户数据不能为空', 'userData');
}
if (!userData.name || userData.name.trim().length === 0) {
throw new ValidationError('姓名不能为空', 'name');
}
if (!userData.email || !isValidEmail(userData.email)) {
throw new ValidationError('邮箱格式不正确', 'email');
}
// 设置默认值
const user = {
name: userData.name.trim(),
email: userData.email.toLowerCase(),
age: userData.age || 0,
isActive: userData.isActive !== undefined ? userData.isActive : true,
createdAt: new Date().toISOString()
};
return user;
}
// 2. 安全的数据访问
function getNestedValue(obj, path, defaultValue = null) {
if (!obj || typeof obj !== 'object') {
return defaultValue;
}
const keys = path.split('.');
let result = obj;
for (const key of keys) {
if (result == null || !(key in result)) {
return defaultValue;
}
result = result[key];
}
return result;
}
// 使用示例
const user = { profile: { address: { city: '北京' } } };
const city = getNestedValue(user, 'profile.address.city', '未知'); // '北京'
const country = getNestedValue(user, 'profile.address.country', '中国'); // '中国'
// 3. 安全的函数调用
function safeCall(fn, defaultValue = null, ...args) {
try {
return fn(...args);
} catch (error) {
console.warn('函数调用失败:', error.message);
return defaultValue;
}
}
// 使用示例
const result = safeCall(JSON.parse, {}, invalidJSONString);
// 4. 超时控制
function withTimeout(promise, timeoutMs, timeoutMessage = '操作超时') {
let timeoutId;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
});
return Promise.race([promise, timeoutPromise])
.finally(() => clearTimeout(timeoutId));
}
// 使用示例
withTimeout(fetch('/api/slow'), 5000)
.then(response => response.json())
.catch(error => {
if (error.message === '操作超时') {
console.log('请求超时,请重试');
} else {
console.error('其他错误:', error);
}
});
// 5. 重试机制
async function retryOperation(operation, maxRetries = 3, delay = 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
console.warn(`尝试 ${attempt} 失败:`, error.message);
if (attempt === maxRetries) break;
await new Promise(resolve => setTimeout(resolve, delay * attempt));
}
}
throw lastError;
}
实践练习
👆 请点击上方按钮进行错误处理演示
选择不同的演示按钮来探索JavaScript错误处理的各种技巧和最佳实践
完整的错误处理示例
// 数据验证服务
class DataValidator {
static validateUserData(userData) {
const errors = [];
// 验证姓名
if (!userData.name || userData.name.trim().length < 2) {
errors.push(new ValidationError('姓名至少需要2个字符', 'name'));
}
// 验证邮箱
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!userData.email || !emailRegex.test(userData.email)) {
errors.push(new EmailValidationError(userData.email));
}
// 验证年龄
if (!userData.age || userData.age < 0 || userData.age > 150) {
errors.push(new ValidationError('年龄必须在0-150之间', 'age'));
}
return errors;
}
}
// 用户服务
class UserService {
static async createUser(userData) {
try {
// 验证数据
const errors = DataValidator.validateUserData(userData);
if (errors.length > 0) {
throw new AggregateError(errors, '用户数据验证失败');
}
// 模拟API调用
const response = await this.makeAPICall('/api/users', {
method: 'POST',
body: JSON.stringify(userData)
});
return response;
} catch (error) {
if (error instanceof AggregateError) {
// 处理多个验证错误
console.error('多个验证错误:');
error.errors.forEach((err, index) => {
console.error(`${index + 1}. ${err.field}: ${err.message}`);
});
} else if (error instanceof NetworkError) {
// 处理网络错误
console.error('网络错误,请检查连接');
} else {
// 处理其他错误
console.error('创建用户失败:', error.message);
}
throw error;
}
}
static async makeAPICall(url, options) {
// 模拟API调用
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.3;
if (success) {
resolve({ id: 123, ...JSON.parse(options.body) });
} else {
reject(new NetworkError('API请求失败', url, 500));
}
}, 1000);
});
}
}
浏览器兼容性
| 特性 | Chrome | Firefox | Safari | Edge | IE |
|---|---|---|---|---|---|
| try/catch | 1.0+ | 1.0+ | 1.0+ | 12+ | 5.5+ |
| Promise.catch | 32+ | 29+ | 8+ | 12+ | 11- |
| async/await | 55+ | 52+ | 10.1+ | 15+ | 11- |
| AggregateError | 85+ | 79+ | 14+ | 85+ | 11- |