JavaScript错误处理

掌握try/catch、错误类型和错误处理最佳实践

为什么需要错误处理?

良好的错误处理可以:

  • 防止程序崩溃
  • 提供有意义的错误信息
  • 增强用户体验
  • 便于调试和维护
  • 提高代码的健壮性
  • 帮助监控应用健康状况

错误处理的重要性

在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-

下一步学习

掌握了错误处理后,可以继续学习: