Day-9-错误处理

Day 9: 错误处理

🎯 学习目标

  • 理解 错误处理 的核心概念和工作原理
  • 掌握相关语法和最佳实践
  • 学会在实际项目中应用 错误处理

💡 核心概念

错误处理是编写健壮程序的关键。JavaScript提供了多种错误处理机制,包括try-catch、throw、Error对象等,帮助我们优雅地处理和调试错误。

关键概念

1. 错误类型

JavaScript有以下常见错误类型:

  1. Error:基础错误类型
  2. SyntaxError:语法错误
eval('1 + * 2');  // SyntaxError
  1. ReferenceError:引用不存在的变量
console.log(notDefined);  // ReferenceError
  1. TypeError:类型错误
null.toString();  // TypeError
  1. RangeError:值超出有效范围
new Array(-1);  // RangeError
  1. 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:实现输入验证

创建一个表单验证函数,要求:

  1. 验证用户对象
function validateUser(user) {
    // 验证规则:
    // - name: 必填,2-20字符
    // - email: 必填,有效邮箱格式
    // - age: 可选,18-120之间
    // - 返回:{valid: boolean, errors: string[]}
}
  1. 抛出ValidationError

  2. 使用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客户端类:

功能要求

  1. 请求拦截
class APIClient {
    constructor(baseURL) {
        this.baseURL = baseURL;
        this.interceptors = [];
    }

    // 添加拦截器
    useInterceptor(fn) {
        this.interceptors.push(fn);
    }

    async request(url, options = {}) {
        // 执行所有拦截器
        // 发送请求
        // 处理响应和错误
    }
}
  1. 错误处理
  • 网络错误:重试3次
  • 4xx错误:抛出ClientError
  • 5xx错误:抛出ServerError
  • 超时错误:抛出TimeoutError
  1. 响应拦截
  • 自动解析JSON
  • 检查响应状态
  • 提取数据
  1. 便捷方法
client.get(url, params)
client.post(url, data)
client.put(url, data)
client.delete(url)
  1. 使用示例
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 小时
关键词: 错误处理