错误处理和异常捕获

在 JavaScript 开发中,错误处理和异常捕获是构建健壮应用程序的关键技术。良好的错误处理机制不仅能提升用户体验,还能帮助开发者快速定位和修复问题。本文将深入探讨 JavaScript 中的错误类型、异常捕获机制以及最佳实践。

一、JavaScript 错误类型体系

1.1 内置错误类型

JavaScript 提供了多种内置错误类型,每种类型代表特定类型的错误:

javascript 复制代码
// Error - 所有错误类型的基类
const baseError = new Error('基本错误信息');

// ReferenceError - 引用未声明变量时抛出
const refError = new ReferenceError('变量未定义');

// TypeError - 变量或参数不是预期类型时抛出
const typeError = new TypeError('类型错误');

// SyntaxError - 语法解析错误
const syntaxError = new SyntaxError('语法错误');

// RangeError - 数值超出有效范围
const rangeError = new RangeError('数值越界');

// URIError - URI处理函数使用不当
const uriError = new URIError('URI处理错误');

// EvalError - eval()函数使用错误(现代JavaScript中已很少使用)
const evalError = new EvalError('eval执行错误');

1.2 自定义错误类型

通过继承 Error 类可以创建自定义错误类型:

javascript 复制代码
class CustomError extends Error {
    constructor(message, errorCode) {
        super(message);
        this.name = 'CustomError';
        this.errorCode = errorCode;
        this.timestamp = new Date().toISOString();
    }
    
    toJSON() {
        return {
            name: this.name,
            message: this.message,
            errorCode: this.errorCode,
            timestamp: this.timestamp,
            stack: this.stack
        };
    }
}

// 使用自定义错误
throw new CustomError('自定义错误消息', 'ERR_001');

二、异常捕获机制

2.1 try-catch-finally 语句

基本的异常捕获结构:

javascript 复制代码
try {
    // 可能抛出异常的代码
    riskyOperation();
} catch (error) {
    // 异常处理
    console.error('捕获到错误:', error.message);
    // 可以选择重新抛出异常
    // throw error;
} finally {
    // 无论是否发生异常都会执行
    cleanupResources();
}

2.2 嵌套的异常处理

javascript 复制代码
function processData(data) {
    try {
        try {
            // 第一层处理
            const parsed = JSON.parse(data);
            return parsed;
        } catch (parseError) {
            // 如果JSON解析失败,尝试其他格式
            if (parseError instanceof SyntaxError) {
                return parseAlternativeFormat(data);
            }
            throw parseError; // 重新抛出非SyntaxError
        }
    } catch (error) {
        // 外层捕获
        logError(error);
        throw new Error('数据处理失败', { cause: error });
    }
}

2.3 Promise 异常处理

Promise 链中的错误处理:

javascript 复制代码
fetchData()
    .then(data => {
        return processData(data);
    })
    .then(processedData => {
        return saveData(processedData);
    })
    .catch(error => {
        // 捕获前面所有步骤中的错误
        console.error('Promise链错误:', error);
        return fallbackOperation();
    })
    .finally(() => {
        // 清理操作
        cleanup();
    });

2.4 async/await 错误处理

使用 async/await 时的错误处理模式:

javascript 复制代码
async function asyncOperation() {
    try {
        const result1 = await step1();
        const result2 = await step2(result1);
        return await step3(result2);
    } catch (error) {
        if (error instanceof NetworkError) {
            await retryOperation();
        } else if (error instanceof ValidationError) {
            await logValidationError(error);
            throw error;
        } else {
            await logUnexpectedError(error);
            throw new Error('操作失败', { cause: error });
        }
    }
}

三、全局错误处理

3.1 window.onerror

全局错误事件处理器:

javascript 复制代码
window.onerror = function(message, source, lineno, colno, error) {
    console.group('全局错误捕获');
    console.log('错误信息:', message);
    console.log('脚本URL:', source);
    console.log('行号:', lineno);
    console.log('列号:', colno);
    console.log('错误对象:', error);
    console.groupEnd();
    
    // 返回true阻止默认错误处理
    return true;
};

3.2 window.addEventListener('error')

更现代的错误监听方式:

javascript 复制代码
window.addEventListener('error', function(event) {
    const { message, filename, lineno, colno, error } = event;
    
    // 发送错误日志到服务器
    sendErrorLog({
        type: 'window_error',
        message,
        filename,
        lineno,
        colno,
        stack: error?.stack,
        timestamp: new Date().toISOString()
    });
    
    // 对于资源加载错误特别处理
    if (event.target && event.target.tagName) {
        handleResourceError(event);
    }
}, true); // 使用捕获阶段

3.3 unhandledrejection 事件

处理未捕获的 Promise 拒绝:

javascript 复制代码
window.addEventListener('unhandledrejection', function(event) {
    const { reason, promise } = event;
    
    console.error('未处理的Promise拒绝:', reason);
    
    // 记录详细的错误信息
    logUnhandledRejection({
        reason: reason instanceof Error ? reason.stack : reason,
        promise: promise.toString(),
        timestamp: new Date().toISOString()
    });
    
    // 阻止浏览器默认的错误提示
    event.preventDefault();
});

四、错误处理最佳实践

4.1 错误分类与处理策略

javascript 复制代码
class ErrorHandler {
    static handle(error) {
        switch (true) {
            case error instanceof NetworkError:
                return this.handleNetworkError(error);
                
            case error instanceof ValidationError:
                return this.handleValidationError(error);
                
            case error instanceof AuthorizationError:
                return this.handleAuthError(error);
                
            case error instanceof DatabaseError:
                return this.handleDatabaseError(error);
                
            default:
                return this.handleUnexpectedError(error);
        }
    }
    
    static handleNetworkError(error) {
        // 网络错误处理逻辑
        if (navigator.onLine) {
            showToast('服务器连接失败,请重试');
        } else {
            showToast('网络连接已断开');
        }
        logError(error);
    }
    
    // 其他错误类型的处理方法...
}

4.2 错误边界(React 应用)

在 React 中使用错误边界:

jsx 复制代码
class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false, error: null };
    }
    
    static getDerivedStateFromError(error) {
        return { hasError: true, error };
    }
    
    componentDidCatch(error, errorInfo) {
        // 记录错误信息
        logErrorToService(error, errorInfo);
    }
    
    render() {
        if (this.state.hasError) {
            return (
                <div className="error-boundary">
                    <h2>Something went wrong.</h2>
                    <details>
                        {this.state.error.toString()}
                        <br />
                        {this.state.error.stack}
                    </details>
                </div>
            );
        }
        
        return this.props.children;
    }
}

4.3 错误日志记录

实现完整的错误日志系统:

javascript 复制代码
class ErrorLogger {
    constructor() {
        this.queue = [];
        this.isSending = false;
    }
    
    log(error, context = {}) {
        const logEntry = {
            id: this.generateId(),
            timestamp: new Date().toISOString(),
            error: this.serializeError(error),
            context,
            userAgent: navigator.userAgent,
            url: window.location.href
        };
        
        this.queue.push(logEntry);
        this.processQueue();
    }
    
    serializeError(error) {
        if (error instanceof Error) {
            return {
                name: error.name,
                message: error.message,
                stack: error.stack,
                ...error
            };
        }
        return { message: String(error) };
    }
    
    async processQueue() {
        if (this.isSending || this.queue.length === 0) return;
        
        this.isSending = true;
        try {
            const logsToSend = this.queue.splice(0, 10);
            await this.sendToServer(logsToSend);
        } catch (sendError) {
            console.error('日志发送失败:', sendError);
            this.queue.unshift(...logsToSend);
        } finally {
            this.isSending = false;
            if (this.queue.length > 0) {
                setTimeout(() => this.processQueue(), 5000);
            }
        }
    }
    
    async sendToServer(logs) {
        // 实际发送到服务器的实现
        return fetch('/api/logs/error', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ logs })
        });
    }
}

五、调试与错误分析技巧

5.1 利用 Source Map

生产环境错误追踪:

javascript 复制代码
// 在构建过程中生成source map
// webpack.config.js
module.exports = {
    devtool: 'source-map',
    // 其他配置...
};

// 错误处理中利用source map
async function mapErrorStack(error) {
    if (!error.stack || !window.sourceMapConsumer) {
        return error.stack;
    }
    
    const stackLines = error.stack.split('\n');
    const mappedStack = await Promise.all(
        stackLines.map(async line => {
            const match = line.match(/\((.*):(\d+):(\d+)\)/);
            if (!match) return line;
            
            const [, url, lineNumber, columnNumber] = match;
            const originalPosition = await sourceMapConsumer.originalPositionFor({
                line: parseInt(lineNumber),
                column: parseInt(columnNumber)
            });
            
            if (originalPosition.source) {
                return line.replace(
                    `${url}:${lineNumber}:${columnNumber}`,
                    `${originalPosition.source}:${originalPosition.line}:${originalPosition.column}`
                );
            }
            return line;
        })
    );
    
    return mappedStack.join('\n');
}

5.2 性能监控与错误关联

javascript 复制代码
class PerformanceErrorCorrelation {
    static init() {
        const performanceEntries = performance.getEntries();
        this.performanceMetrics = {
            dns: performanceEntries.find(e => e.entryType === 'dns')?.duration,
            tcp: performanceEntries.find(e => e.entryType === 'connect')?.duration,
            ttfb: performanceEntries.find(e => e.entryType === 'response')?.duration,
            // 其他性能指标...
        };
    }
    
    static correlateErrorWithPerformance(error) {
        return {
            error: error.toString(),
            performance: this.performanceMetrics,
            memory: performance.memory ? {
                usedJSHeapSize: performance.memory.usedJSHeapSize,
                totalJSHeapSize: performance.memory.totalJSHeapSize
            } : null
        };
    }
}

// 在错误发生时记录性能数据
window.addEventListener('error', event => {
    const correlatedData = PerformanceErrorCorrelation.correlateErrorWithPerformance(event.error);
    errorLogger.log(event.error, correlatedData);
});

六、测试中的错误处理

6.1 单元测试中的错误断言

javascript 复制代码
// 使用Jest进行错误测试
describe('Error handling', () => {
    test('should throw specific error', () => {
        // 测试同步函数抛出错误
        expect(() => riskyFunction()).toThrow(ExpectedError);
        expect(() => riskyFunction()).toThrow('特定的错误信息');
        
        // 测试异步函数拒绝
        await expect(asyncFunction()).rejects.toThrow(NetworkError);
    });
    
    test('should handle error correctly', async () => {
        const consoleSpy = jest.spyOn(console, 'error');
        const error = new Error('测试错误');
        
        await ErrorHandler.handle(error);
        
        expect(consoleSpy).toHaveBeenCalledWith(
            expect.stringContaining('测试错误')
        );
    });
});

6.2 E2E 测试中的错误场景

javascript 复制代码
// 使用Cypress进行端到端错误测试
describe('Error scenarios', () => {
    it('should display error page on network failure', () => {
        // 拦截网络请求并模拟失败
        cy.intercept('GET', '/api/data', {
            statusCode: 500,
            body: { error: '服务器错误' }
        });
        
        cy.visit('/data-page');
        cy.contains('服务器连接失败').should('be.visible');
    });
    
    it('should handle client-side errors gracefully', () => {
        // 注入错误脚本测试错误边界
        cy.visit('/app');
        cy.window().then(win => {
            win.injectError(new Error('注入的错误'));
        });
        
        cy.get('.error-boundary').should('be.visible');
    });
});

结语

JavaScript 错误处理与异常捕获是一个复杂但至关重要的主题。通过深入理解错误类型体系、掌握各种异常捕获机制、实施全局错误处理策略、遵循最佳实践以及建立完善的错误监控系统,开发者可以构建出更加健壮和可靠的前端应用程序。

记住,优秀的错误处理不仅仅是捕获和记录错误,更重要的是如何从错误中恢复、如何向用户提供有意义的反馈,以及如何利用错误信息来持续改进应用程序的质量。

关键要点总结:

  • 了解不同类型的错误及其适用场景
  • 使用 try-catch-finally 处理同步错误
  • 使用 .catch() 和 async/await 处理异步错误
  • 实现全局错误处理以捕获未处理的异常
  • 建立错误分类和处理策略
  • 实施完善的错误日志记录和监控系统
  • 在测试中覆盖错误场景

通过系统性地应用这些技术和策略,您将能够创建出具有高度容错能力和优秀用户体验的 JavaScript 应用程序。