Day 14 - ArkTS 错误处理

目标:掌握 throw、try/catch/finally,学会自定义异常类,理解错误处理最佳实践
预计时间:1.5-2小时
前置知识:Day 01-13 所有内容(基础语法、函数、类、继承、接口、泛型、枚举、Type别名)


课前思考

回顾前面学习的函数和类:

// 我们写过很多函数
function divide(a: number, b: number): number {
    return a / b;
}

// 也写过很多类
class BankAccount {
    private _balance: number = 0;
    
    withdraw(amount: number): void {
        this._balance = this._balance - amount;
    }
}

思考问题:

  1. 如果调用 divide(10, 0) 会发生什么?程序崩溃还是返回 Infinity?
  2. 如果账户余额不足却调用 withdraw(1000),如何通知调用方"操作失败"?
  3. C++ 用异常处理这类问题,ArkTS 呢?

第一部分:为什么需要错误处理

1.1 程序运行中的意外情况

问题引入:程序不可能永远按预期运行

function readFile(path: string): string {
    // 假设这里读取文件
    // 问题:文件不存在怎么办?
    // 问题:没有权限怎么办?
    // 问题:磁盘损坏怎么办?
    return "文件内容";
}

function connectToServer(ip: string, port: number): void {
    // 问题:网络不通怎么办?
    // 问题:服务器拒绝连接怎么办?
    // 问题:连接超时怎么办?
}

这些"意外"的共同特点:

  • 不是程序逻辑错误(不是 bug)
  • 是运行时环境导致的失败
  • 需要有一种机制通知调用方"出错了"

1.2 C++ 的异常处理回顾

作为 C++ 开发者,你对这套机制很熟悉:

// C++ 异常处理
#include <stdexcept>

double divide(double a, double b) {
    if (b == 0) {
        throw std::runtime_error("除数不能为零");
    }
    return a / b;
}

int main() {
    try {
        double result = divide(10, 0);
        std::cout << "结果:" << result << std::endl;
    } catch (const std::runtime_error& e) {
        std::cerr << "捕获异常:" << e.what() << std::endl;
    }
    return 0;
}

C++ 异常处理的核心要素:

  • throw:抛出异常
  • try:包裹可能抛出异常的代码
  • catch:捕获并处理异常
  • 异常类:通常继承自 std::exception

1.3 错误码 vs 异常 —— 两种策略对比

策略一:错误码(C 语言风格)

// 返回错误码,通过参数返回结果
function divideWithCode(a: number, b: number, result: number[]): number {
    if (b === 0) {
        return -1; // 错误码:除零错误
    }
    result.push(a / b);
    return 0; // 成功
}

// 使用方必须检查错误码
let result: number[] = [];
let code: number = divideWithCode(10, 0, result);
if (code !== 0) {
    console.log(`错误:除零错误`);
} else {
    console.log(`结果:${result[0]}`);
}

错误码的缺点:

  • 调用方可能忘记检查错误码
  • 错误码含义不直观(-1 代表什么?)
  • 多层调用时错误码需要逐层传递
  • 正常逻辑和错误处理混在一起

策略二:异常(现代语言倾向)

// 底层抛出异常
function divideWithException(a: number, b: number): number {
    if (b === 0) {
        throw new Error("除数不能为零");
    }
    return a / b;
}

// 中间层不处理
function middleLayer(x: number): number {
    return divideWithException(x, 0) + 1;  // 异常自动向上传
}

// 上层统一处理
try {
    middleLayer(10);  // 底层的异常传到这里才被捕获
} catch (e) {
    console.log(`捕获异常:${String(e)}`);
}

异常的优势:

  • 强制处理(不 catch 程序就崩溃)
  • 错误信息丰富(可以携带任意信息)
  • 自动向上传播(中间层不需要写任何处理代码)
  • 正常逻辑和错误处理分离

对比错误码 vs 异常:

方式 中间层代码 特点
错误码 必须检查并传递错误码 繁琐,容易遗漏
异常 什么都不用写 自动传播到上层

ArkTS 的选择: 主要使用异常机制,但简单场景也可以用返回值。


第二部分:throw 抛出异常

2.1 throw 语法

基本形式:

// 抛出 Error 对象
throw new Error("错误描述信息");

// 抛出带特定消息的 Error
throw new Error("文件不存在:/data/config.txt");

// 可以在任何地方抛出
function checkAge(age: number): void {
    if (age < 0) {
        throw new Error("年龄不能为负数");
    }
    if (age > 150) {
        throw new Error("年龄超出合理范围");
    }
}

throw 的重要特性:throw 后的代码不再执行

function testThrow(): void {
    console.log("第一行");
    throw new Error("出错了");
    console.log("第二行");  // ❌ 永远不会执行!
    console.log("第三行");  // ❌ 永远不会执行!
}

// 调用
testThrow();
// 输出:第一行
//       然后抛出异常,函数立即终止

throw 的行为:

  • 立即中断当前函数的执行
  • 跳转到调用栈上层的最近 catch 块
  • throw 之后的代码永远不会执行

对比 C++:

特性 C++ ArkTS
抛出语法 throw MyException(); throw new Error("msg");
抛出类型 任意类型(推荐异常类) 必须是 Error 或其子类
抛出指针 throw new MyException()(不推荐) throw new Error()(标准做法)
执行中断

2.2 Error 对象

Error 对象的属性:

let err: Error = new Error("这是一个错误");

// message 属性:错误描述
console.log(`message: ${err.message}`); // "这是一个错误"

// name 属性:错误类型名称
console.log(`name: ${err.name}`); // "Error"

创建 Error 的完整形式:

// 只有 message 参数
let err1: Error = new Error("简单错误");

// 查看所有属性
function printError(err: Error): void {
    console.log(`错误名称:${err.name}`);
    console.log(`错误消息:${err.message}`);
}

对比 C++:

// C++ 标准异常类
std::runtime_error err("这是一个错误");
std::cout << err.what();  // 获取错误消息

// ArkTS 对应
let err: Error = new Error("这是一个错误");
console.log(err.message);  // 获取错误消息

2.3 何时该抛出异常

抛出异常的原则:异常应表示"不该发生的情况"

// ✅ 应该抛出异常的情况:违反前置条件
function sqrt(x: number): number {
    if (x < 0) {
        throw new Error("不能对负数开平方");
    }
    return Math.sqrt(x);
}

// ✅ 应该抛出异常的情况:违反业务规则
class BankAccount {
    private _balance: number = 0;
    
    withdraw(amount: number): void {
        if (amount < 0) {
            throw new Error("取款金额不能为负数");
        }
        if (amount > this._balance) {
            throw new Error("余额不足");
        }
        this._balance = this._balance - amount;
    }
}

// ✅ 应该抛出异常的情况:资源访问失败
function readConfig(path: string): string {
    if (path === "") {
        throw new Error("配置文件路径不能为空");
    }
    // 模拟读取失败
    throw new Error(`无法读取文件:${path}`);
}

不应该抛出异常的情况:

// ❌ 不要用异常处理正常流程
function findUser(users: string[], name: string): string {
    for (let i = 0; i < users.length; i++) {
        if (users[i] === name) {
            return users[i];
        }
    }
    throw new Error("用户不存在");  // 不好!找不到是正常情况
}

// ✅ 用返回值表示"找不到"
function findUserBetter(users: string[], name: string): string | null {
    for (let i = 0; i < users.length; i++) {
        if (users[i] === name) {
            return users[i];
        }
    }
    return null;  // 找不到返回 null
}

第三部分:try / catch / finally

3.1 基本语法

三个代码块的作用:

// try:包裹可能抛出异常的代码
// catch:捕获并处理异常
// finally:无论是否异常都执行(用于清理资源)

try {
    // 可能抛出异常的代码
    let result: number = divide(10, 0);
    console.log(`结果:${result}`);
} catch (e) {
    // 异常处理代码
    console.log(`捕获到异常:${String(e)}`);
} finally {
    // 清理代码(可选)
    console.log("清理资源...");
}

function divide(a: number, b: number): number {
    if (b === 0) {
        throw new Error("除数不能为零");
    }
    return a / b;
}

执行流程:

  1. 正常情况:try → finally
  2. 异常情况:try(抛出)→ catch → finally
  3. catch 中再抛出:try(抛出)→ catch(抛出)→ finally → 向上传播

3.2 catch 中的错误对象

获取错误信息:

try {
    throw new Error("发生错误");
} catch (e) {
    // e 的类型在 ArkTS 中是 Object
    // 需要转换为 Error 类型来访问 message
    if (e instanceof Error) {
        console.log(`错误消息:${e.message}`);
        console.log(`错误名称:${e.name}`);
    } else {
        console.log(`未知错误:${String(e)}`);
    }
}

使用 instanceof 判断错误类型:

class ValidationError extends Error {
    constructor(message: string) {
        super(message);
        this.name = "ValidationError";
    }
}

class NetworkError extends Error {
    constructor(message: string) {
        super(message);
        this.name = "NetworkError";
    }
}

function processError(e: Object): void {
    if (e instanceof ValidationError) {
        console.log(`验证错误:${e.message}`);
    } else if (e instanceof NetworkError) {
        console.log(`网络错误:${e.message}`);
    } else if (e instanceof Error) {
        console.log(`一般错误:${e.message}`);
    } else {
        console.log(`未知错误:${String(e)}`);
    }
}

对比 C++:

// C++ 用引用捕获异常
try {
    throw MyException("错误");
} catch (const MyException& e) {
    std::cout << e.what();
} catch (const std::exception& e) {
    std::cout << e.what();
} catch (...) {
    std::cout << "未知异常";
}

// ArkTS 用 instanceof 判断类型
try {
    throw new MyError("错误");
} catch (e) {
    if (e instanceof MyError) {
        console.log(e.message);
    } else if (e instanceof Error) {
        console.log(e.message);
    }
}

3.3 finally 资源清理

finally 的核心特性:必定执行

无论发生什么情况,finally 块一定会执行

  • 正常结束 ✅
  • 发生异常 ✅
  • try 中有 return ✅
  • catch 中重新抛出异常 ✅
function processFile(path: string): void {
    let resource: string = "文件句柄";
    
    try {
        console.log(`打开资源:${resource}`);
        // 处理文件...
        if (path === "") {
            throw new Error("路径为空");
        }
        console.log("处理完成");
        return;  // 即使有 return,finally 也会执行
    } catch (e) {
        console.log(`处理异常:${String(e)}`);
    } finally {
        // 无论是否异常、是否有 return,这里都会执行
        console.log(`释放资源:${resource}`);
    }
}

// 调用测试
processFile("");  // 异常路径
processFile("/data/file.txt");  // 正常路径

输出:

打开资源:文件句柄
处理异常:Error: 路径为空
释放资源:文件句柄
打开资源:文件句柄
处理完成
释放资源:文件句柄

对比 C++ RAII:

// C++ 推荐用 RAII(资源获取即初始化)
class FileHandle {
public:
    FileHandle(const string& path) { /* 打开文件 */ }
    ~FileHandle() { /* 自动关闭文件 */ }  // 析构时释放
};

void processFile(const string& path) {
    FileHandle fh(path);  // 栈对象,退出作用域自动析构
    // 处理文件...
}  // fh 自动销毁,文件自动关闭

关键区别:

  • C++:用析构函数实现自动清理(RAII)
  • ArkTS:没有析构函数,用 finally 显式清理

3.4 嵌套 try/catch

多层异常处理:

function level3(): void {
    throw new Error("底层错误");
}

function level2(): void {
    try {
        level3();
    } catch (e) {
        console.log("level2 捕获到错误,包装后重新抛出");
        throw new Error(`level2包装: ${String(e)}`);
    }
}

function level1(): void {
    try {
        level2();
    } catch (e) {
        console.log(`level1 最终处理:${String(e)}`);
    }
}

level1();

输出:

level2 捕获到错误,包装后重新抛出
level1 最终处理:Error: level2包装: Error: 底层错误

异常传播链:

class DatabaseError extends Error {
    constructor(message: string, public readonly sql: string) {
        super(message);
        this.name = "DatabaseError";
    }
}

class ServiceError extends Error {
    constructor(message: string, public readonly cause: Error) {
        super(message);
        this.name = "ServiceError";
    }
}

function queryDatabase(sql: string): void {
    throw new DatabaseError("连接超时", sql);
}

function fetchUserData(userId: number): void {
    try {
        queryDatabase(`SELECT * FROM users WHERE id=${userId}`);
    } catch (e) {
        if (e instanceof DatabaseError) {
            // 包装成业务异常,保留原始信息
            throw new ServiceError("获取用户数据失败", e);
        }
        throw e;
    }
}

try {
    fetchUserData(123);
} catch (e) {
    if (e instanceof ServiceError) {
        console.log(`业务错误:${e.message}`);
        console.log(`原始错误:${e.cause.message}`);
        console.log(`SQL:${(e.cause as DatabaseError).sql}`);
    }
}

第四部分:自定义异常类

4.1 为什么需要自定义异常

问题:所有异常都是 Error,无法区分类型

// 无法区分是哪种错误
try {
    // 可能抛出各种错误
    connectDatabase();
    parseConfig();
    validateUser();
} catch (e) {
    // 都是 Error,怎么知道是哪个环节出错?
    console.log(`错误:${e.message}`);
}

解决方案:自定义异常类

// 每种错误有自己的类型
class DatabaseConnectionError extends Error {
    constructor(message: string) {
        super(message);
        this.name = "DatabaseConnectionError";
    }
}

class ConfigParseError extends Error {
    constructor(message: string, public readonly filePath: string) {
        super(message);
        this.name = "ConfigParseError";
    }
}

class UserValidationError extends Error {
    constructor(message: string, public readonly field: string) {
        super(message);
        this.name = "UserValidationError";
    }
}

4.2 extends Error —— 创建业务异常类

自定义异常的基本结构:

// 基础业务异常
class BusinessError extends Error {
    constructor(message: string) {
        super(message);
        this.name = "BusinessError";
    }
}

// 具体的业务异常
class InsufficientBalanceError extends BusinessError {
    constructor(
        message: string,
        public readonly currentBalance: number,
        public readonly requiredAmount: number
    ) {
        super(message);
        this.name = "InsufficientBalanceError";
    }
}

class InvalidAccountError extends BusinessError {
    constructor(
        message: string,
        public readonly accountId: string
    ) {
        super(message);
        this.name = "InvalidAccountError";
    }
}

// 使用
class BankAccount {
    private _balance: number = 100;
    private _accountId: string = "ACC001";
    
    withdraw(amount: number): void {
        if (amount <= 0) {
            throw new BusinessError("取款金额必须大于0");
        }
        if (amount > this._balance) {
            throw new InsufficientBalanceError(
                "余额不足",
                this._balance,
                amount
            );
        }
        this._balance = this._balance - amount;
    }
}

// 处理时区分类型
let account: BankAccount = new BankAccount();
try {
    account.withdraw(200);
} catch (e) {
    if (e instanceof InsufficientBalanceError) {
        console.log(`余额不足:当前${e.currentBalance},需要${e.requiredAmount}`);
    } else if (e instanceof BusinessError) {
        console.log(`业务错误:${e.message}`);
    } else {
        console.log(`系统错误:${String(e)}`);
    }
}

对比 C++:

// C++ 继承 std::exception
class MyException : public std::exception {
private:
    std::string msg;
public:
    MyException(const std::string& m) : msg(m) {}
    const char* what() const noexcept override {
        return msg.c_str();
    }
};

// ArkTS 继承 Error
class MyError extends Error {
    constructor(message: string) {
        super(message);
        this.name = "MyError";
    }
}

4.3 instanceof 区分异常类型

在 catch 中判断异常类型:

class NetworkTimeoutError extends Error {
    constructor(message: string, public readonly timeoutMs: number) {
        super(message);
        this.name = "NetworkTimeoutError";
    }
}

class NetworkConnectionError extends Error {
    constructor(message: string, public readonly host: string) {
        super(message);
        this.name = "NetworkConnectionError";
    }
}

function handleNetworkRequest(): void {
    // 模拟不同的网络错误
    let random: number = Math.random();
    if (random < 0.33) {
        throw new NetworkTimeoutError("连接超时", 5000);
    } else if (random < 0.66) {
        throw new NetworkConnectionError("连接被拒绝", "api.example.com");
    } else {
        throw new Error("未知网络错误");
    }
}

try {
    handleNetworkRequest();
} catch (e) {
    // 按类型分别处理
    if (e instanceof NetworkTimeoutError) {
        console.log(`请求超时(${e.timeoutMs}ms),建议重试`);
    } else if (e instanceof NetworkConnectionError) {
        console.log(`无法连接到 ${e.host},请检查网络`);
    } else if (e instanceof Error) {
        console.log(`网络错误:${e.message}`);
    }
}

instanceof 的工作原理:

// instanceof 检查原型链
class BaseError extends Error {}
class DerivedError extends BaseError {}

let err: DerivedError = new DerivedError("测试");

console.log(`${err instanceof DerivedError}`);  // true
console.log(`${err instanceof BaseError}`);     // true(继承链)
console.log(`${err instanceof Error}`);         // true(继承链)
console.log(`${err instanceof Object}`);        // true(所有类的基类)

4.4 异常层级设计

设计良好的异常层次结构:

// 1. 应用基础异常(所有应用异常的基类)
class ApplicationError extends Error {
    constructor(message: string, public readonly code: string) {
        super(message);
        this.name = "ApplicationError";
    }
}

// 2. 领域层异常
class DomainError extends ApplicationError {
    constructor(message: string, code: string) {
        super(message, code);
        this.name = "DomainError";
    }
}

// 3. 基础设施层异常
class InfrastructureError extends ApplicationError {
    constructor(message: string, code: string) {
        super(message, code);
        this.name = "InfrastructureError";
    }
}

// 4. 具体的领域异常
class InvalidOrderStateError extends DomainError {
    constructor(message: string) {
        super(message, "ORDER_001");
        this.name = "InvalidOrderStateError";
    }
}

class ProductOutOfStockError extends DomainError {
    constructor(message: string, public readonly productId: string) {
        super(message, "ORDER_002");
        this.name = "ProductOutOfStockError";
    }
}

// 5. 具体的基础设施异常
class DatabaseConnectionError extends InfrastructureError {
    constructor(message: string) {
        super(message, "DB_001");
        this.name = "DatabaseConnectionError";
    }
}

class CacheUnavailableError extends InfrastructureError {
    constructor(message: string) {
        super(message, "CACHE_001");
        this.name = "CacheUnavailableError";
    }
}

// 使用示例
class OrderService {
    placeOrder(productId: string, quantity: number): void {
        if (quantity <= 0) {
            throw new InvalidOrderStateError("订单数量必须大于0");
        }
        // 模拟库存检查
        if (productId === "OUT_OF_STOCK") {
            throw new ProductOutOfStockError("商品缺货", productId);
        }
        console.log("订单创建成功");
    }
}

// 统一异常处理
function handleServiceError(e: Object): void {
    if (e instanceof DomainError) {
        console.log(`[业务错误 ${e.code}] ${e.message}`);
    } else if (e instanceof InfrastructureError) {
        console.log(`[系统错误 ${e.code}] ${e.message},请联系管理员`);
    } else if (e instanceof Error) {
        console.log(`[未知错误] ${e.message}`);
    }
}

第五部分:错误处理最佳实践

5.1 何时用异常 vs 何时用返回值

使用异常的场景:

// ✅ 违反前置条件
function sqrt(x: number): number {
    if (x < 0) {
        throw new Error("不能对负数开平方");
    }
    return Math.sqrt(x);
}

// ✅ 违反不变量
class Stack<T> {
    private items: T[] = [];
    
    pop(): T {
        if (this.items.length === 0) {
            throw new Error("栈为空");
        }
        return this.items.pop() as T;
    }
}

// ✅ 资源访问失败
function readFile(path: string): string {
    if (path === "") {
        throw new Error("路径不能为空");
    }
    // 读取失败抛出异常
    throw new Error("文件不存在");
}

使用返回值的场景:

// ✅ 查找可能不存在
function findUserById(users: string[], id: number): string | null {
    if (id >= 0 && id < users.length) {
        return users[id];
    }
    return null;  // 找不到是正常的
}

// ✅ 解析可能失败
function parseNumber(str: string): number | null {
    let num: number = Number(str);
    if (isNaN(num)) {
        return null;  // 解析失败是正常的
    }
    return num;
}

// ✅ 业务校验结果
function validatePassword(password: string): { valid: boolean; errors: string[] } {
    let errors: string[] = [];
    if (password.length < 8) {
        errors.push("密码至少8位");
    }
    // ... 其他检查
    return { valid: errors.length === 0, errors: errors };
}

决策指南:

场景 推荐方式
违反前置条件/不变量 抛出异常
资源访问失败 抛出异常
查找可能不存在 返回 null/undefined
解析可能失败 返回 null 或结果对象
业务校验(多错误) 返回错误列表
状态转换失败 抛出异常

5.2 不要忽略异常

❌ 危险的空 catch:

// ❌ 绝对不要这样做!
try {
    criticalOperation();
} catch (e) {
    // 什么都不做,异常被吞掉了!
}

// ❌ 只打印日志但不处理
try {
    saveData();
} catch (e) {
    console.log("出错了");  // 程序继续执行,但数据没保存!
}
continueWithInconsistentState();  // 在错误状态下继续!

✅ 正确处理异常:

// ✅ 至少记录详细信息
try {
    criticalOperation();
} catch (e) {
    console.log(`操作失败:${String(e)}`);
    // 根据情况决定是否继续
    throw e;  // 重新抛出,让上层处理
}

// ✅ 转换为业务结果
try {
    let data: string = loadConfig();
    processConfig(data);
} catch (e) {
    console.log(`加载配置失败,使用默认配置:${String(e)}`);
    useDefaultConfig();  // 有备选方案
}

// ✅ 包装后抛出
try {
    lowLevelOperation();
} catch (e) {
    throw new BusinessError(`业务操作失败:${String(e)}`);
}

5.3 错误信息的设计

好的错误信息:

// ✅ 清晰说明问题
throw new Error("除数不能为零");

// ✅ 包含上下文信息
throw new Error(`用户 ${userId} 不存在`);

// ✅ 提供解决建议
throw new Error(`配置文件 ${path} 不存在,请检查路径或运行 init 命令创建`);

// ✅ 包含相关数据
class ValidationError extends Error {
    constructor(
        message: string,
        public readonly field: string,
        public readonly value: string
    ) {
        super(`${field} 验证失败:${message},当前值:${value}`);
        this.name = "ValidationError";
    }
}

不好的错误信息:

// ❌ 太模糊
throw new Error("出错了");
throw new Error("操作失败");

// ❌ 技术细节暴露给用户
throw new Error("Array index out of bounds: 5");

// ❌ 没有上下文
throw new Error("文件不存在");  // 哪个文件?

5.4 防御性编程

参数校验:

class SafeCalculator {
    divide(a: number, b: number): number {
        // 防御性校验
        if (typeof a !== "number" || typeof b !== "number") {
            throw new Error("参数必须是数字");
        }
        if (b === 0) {
            throw new Error("除数不能为零");
        }
        return a / b;
    }
    
    sqrt(x: number): number {
        if (x < 0) {
            throw new Error("不能对负数开平方");
        }
        return Math.sqrt(x);
    }
}

边界检查:

class SafeArray<T> {
    private items: T[] = [];
    
    get(index: number): T {
        if (index < 0 || index >= this.items.length) {
            throw new Error(`索引 ${index} 超出范围 [0, ${this.items.length})`);
        }
        return this.items[index];
    }
    
    add(item: T): void {
        if (item === null || item === undefined) {
            throw new Error("不能添加 null 或 undefined");
        }
        this.items.push(item);
    }
}

前置条件检查:

class BankTransfer {
    transfer(from: string, to: string, amount: number): void {
        // 前置条件检查
        this.assertNotEmpty(from, "转出账户");
        this.assertNotEmpty(to, "转入账户");
        this.assertPositive(amount, "转账金额");
        this.assertNotSame(from, to, "转出和转入账户");
        
        // 执行业务逻辑
        // ...
    }
    
    private assertNotEmpty(value: string, name: string): void {
        if (value === "") {
            throw new Error(`${name}不能为空`);
        }
    }
    
    private assertPositive(value: number, name: string): void {
        if (value <= 0) {
            throw new Error(`${name}必须大于0`);
        }
    }
    
    private assertNotSame(a: string, b: string, name: string): void {
        if (a === b) {
            throw new Error(`${name}不能相同`);
        }
    }
}

第六部分:小结与练习

6.1 知识点对比总结表

概念 ArkTS C++
抛出异常 throw new Error("msg") throw MyException("msg")
异常基类 Error std::exception
捕获异常 catch (e) { ... } catch (const MyEx& e) { ... }
类型判断 e instanceof MyError 多个 catch 块
自定义异常 class MyError extends Error class MyEx : public std::exception
资源清理 finally RAII(析构函数)
嵌套异常 支持 支持
异常规格 noexcept

6.2 核心要点回顾

  1. throw:抛出 Error 对象或其子类实例
  2. try/catch/finally:捕获和处理异常,finally 用于清理
  3. 自定义异常:继承 Error 类,添加业务属性
  4. instanceof:在 catch 中判断异常类型
  5. 最佳实践:异常用于"不该发生的情况",不要忽略异常,设计好的错误信息

练习题

练习1:选择题

  1. 以下哪个是 ArkTS 中抛出异常的正确语法?

    • A. throw "错误信息"
    • B. throw new Error("错误信息")
    • C. throw Error("错误信息")
    • D. throw "Error", "错误信息"
  2. 在 catch 块中,如何判断捕获的异常是特定自定义类型?

    • A. if (e.type === "MyError")
    • B. if (e instanceof MyError)
    • C. if (e.name === "MyError")
    • D. if (typeof e === "MyError")
  3. finally 块的特点是?

    • A. 只有发生异常时才执行
    • B. 只有没有异常时才执行
    • C. 无论是否发生异常都会执行
    • D. 只有 return 时才执行
练习1答案

答案:1.B 2.B 3.C

题号 答案 解析
1 B ArkTS 要求抛出 Error 对象实例,使用 new Error()
2 B 使用 instanceof 运算符检查对象类型
3 C finally 块无论是否异常、是否有 return 都会执行

练习2:代码补全

补全以下代码,实现一个带异常处理的除法函数:

// 1. 定义自定义异常类 DivideByZeroError
class DivideByZeroError extends ______ {
    constructor() {
        super("除数不能为零");
        this.______ = "DivideByZeroError";
    }
}

// 2. 实现 safeDivide 函数
function safeDivide(a: number, b: number): ______ {
    if (______ === 0) {
        throw new ______();
    }
    return a / b;
}

// 3. 使用 try/catch 调用
______ {
    let result: number = safeDivide(10, 0);
    console.log(`结果:${result}`);
} ______ (e) {
    if (e ______ DivideByZeroError) {
        console.log(`除零错误:${e.message}`);
    } else {
        console.log(`其他错误:${String(e)}`);
    }
}
练习2答案
// 1. 定义自定义异常类 DivideByZeroError
class DivideByZeroError extends Error {
    constructor() {
        super("除数不能为零");
        this.name = "DivideByZeroError";
    }
}

// 2. 实现 safeDivide 函数
function safeDivide(a: number, b: number): number {
    if (b === 0) {
        throw new DivideByZeroError();
    }
    return a / b;
}

// 3. 使用 try/catch 调用
try {
    let result: number = safeDivide(10, 0);
    console.log(`结果:${result}`);
} catch (e) {
    if (e instanceof DivideByZeroError) {
        console.log(`除零错误:${e.message}`);
    } else {
        console.log(`其他错误:${String(e)}`);
    }
}

练习3:编程题 - 实现带异常处理的栈

要求实现一个 SafeStack<T> 类:

  1. 使用泛型 T
  2. 私有属性 items: T[] 存储数据
  3. 方法 push(item: T): void —— 添加元素
  4. 方法 pop(): T —— 弹出元素,栈空时抛出 StackEmptyError
  5. 方法 peek(): T —— 查看栈顶,栈空时抛出 StackEmptyError
  6. getter isEmpty(): boolean
  7. getter size(): number

定义自定义异常 StackEmptyError

练习3答案
// 自定义异常
class StackEmptyError extends Error {
    constructor() {
        super("栈为空");
        this.name = "StackEmptyError";
    }
}

// 安全栈实现
class SafeStack<T> {
    private items: T[] = [];
    
    push(item: T): void {
        this.items.push(item);
    }
    
    pop(): T {
        if (this.items.length === 0) {
            throw new StackEmptyError();
        }
        // 不使用 as 类型断言,避免使用未学语法
        let len = this.items.length;
        let e = this.items[len - 1];
        this.items.splice(len - 1, 1);
        return e;
    }
    
    peek(): T {
        if (this.items.length === 0) {
            throw new StackEmptyError();
        }
        return this.items[this.items.length - 1];
    }
    
    get isEmpty(): boolean {
        return this.items.length === 0;
    }
    
    get size(): number {
        return this.items.length;
    }
}

// 使用示例
let stack: SafeStack<number> = new SafeStack<number>();
stack.push(10);
stack.push(20);
console.log(`栈顶:${stack.peek()}`);  // 20
console.log(`弹出:${stack.pop()}`);    // 20
console.log(`大小:${stack.size}`);     // 1

try {
    stack.pop();
    stack.pop();  // 这里会抛出异常
} catch (e) {
    if (e instanceof StackEmptyError) {
        console.log(`错误:${e.message}`);
    }
}

练习4:编程题 - 实现文件读取器

实现一个 FileReader 类,带异常处理:

  1. 定义 FileNotFoundError 异常(继承 Error)
  2. 定义 PermissionDeniedError 异常(继承 Error)
  3. FileReader 有私有属性 path: string
  4. 构造函数接收 path,如果为空字符串抛出 Error
  5. 方法 read(): string,模拟读取:
    • 如果 path 包含 "notfound" 抛出 FileNotFoundError
    • 如果 path 包含 "denied" 抛出 PermissionDeniedError
    • 否则返回 "文件内容"
  6. 使用 try/catch/finally 调用,finally 中打印 "清理资源"
练习4答案
// 自定义异常
class FileNotFoundError extends Error {
    constructor(public readonly path: string) {
        super(`文件不存在:${path}`);
        this.name = "FileNotFoundError";
    }
}

class PermissionDeniedError extends Error {
    constructor(public readonly path: string) {
        super(`没有权限访问:${path}`);
        this.name = "PermissionDeniedError";
    }
}

// 文件读取器
class FileReader {
    private path: string;
    
    constructor(path: string) {
        if (path === "") {
            throw new Error("文件路径不能为空");
        }
        this.path = path;
    }
    
    read(): string {
        if (this.path.indexOf("notfound") >= 0) {
            throw new FileNotFoundError(this.path);
        }
        if (this.path.indexOf("denied") >= 0) {
            throw new PermissionDeniedError(this.path);
        }
        return "文件内容";
    }
}

// 使用示例
function readFileSafely(path: string): void {
    let reader: FileReader | null = null;
    
    try {
        reader = new FileReader(path);
        let content: string = reader.read();
        console.log(`读取成功:${content}`);
    } catch (e) {
        if (e instanceof FileNotFoundError) {
            console.log(`文件未找到:${e.path}`);
        } else if (e instanceof PermissionDeniedError) {
            console.log(`权限不足:${e.path}`);
        } else if (e instanceof Error) {
            console.log(`错误:${e.message}`);
        }
    } finally {
        console.log("清理资源");
    }
}

// 测试
readFileSafely("/data/test.txt");        // 正常
readFileSafely("/data/notfound.txt");    // 文件不存在
readFileSafely("/data/denied.txt");      // 权限不足
readFileSafely("");                       // 路径为空

练习5:编程题 - 银行账户异常体系

实现一个完整的银行账户异常体系:

  1. 定义基类 AccountError 继承 Error
  2. 定义子类:
    • InsufficientFundsError(余额不足)—— 属性:currentBalance, requiredAmount
    • InvalidAmountError(无效金额)—— 属性:amount
    • AccountFrozenError(账户冻结)—— 属性:reason
  3. 实现 BankAccount 类:
    • 属性:balance(余额), frozen(是否冻结)
    • 方法:deposit(amount) —— 存款
    • 方法:withdraw(amount) —— 取款,可能抛出各种异常
  4. 编写处理函数,根据异常类型给出不同提示
练习5答案
// 异常体系
class AccountError extends Error {
    constructor(message: string) {
        super(message);
        this.name = "AccountError";
    }
}

class InsufficientFundsError extends AccountError {
    constructor(
        public readonly currentBalance: number,
        public readonly requiredAmount: number
    ) {
        super(`余额不足:当前 ${currentBalance},需要 ${requiredAmount}`);
        this.name = "InsufficientFundsError";
    }
}

class InvalidAmountError extends AccountError {
    constructor(public readonly amount: number) {
        super(`无效金额:${amount}`);
        this.name = "InvalidAmountError";
    }
}

class AccountFrozenError extends AccountError {
    constructor(public readonly reason: string) {
        super(`账户已冻结:${reason}`);
        this.name = "AccountFrozenError";
    }
}

// 银行账户
class BankAccount {
    private _balance: number = 0;
    private _frozen: boolean = false;
    private _freezeReason: string = "";
    
    get balance(): number {
        return this._balance;
    }
    
    freeze(reason: string): void {
        this._frozen = true;
        this._freezeReason = reason;
    }
    
    unfreeze(): void {
        this._frozen = false;
        this._freezeReason = "";
    }
    
    deposit(amount: number): void {
        if (amount <= 0) {
            throw new InvalidAmountError(amount);
        }
        if (this._frozen) {
            throw new AccountFrozenError(this._freezeReason);
        }
        this._balance = this._balance + amount;
    }
    
    withdraw(amount: number): void {
        if (amount <= 0) {
            throw new InvalidAmountError(amount);
        }
        if (this._frozen) {
            throw new AccountFrozenError(this._freezeReason);
        }
        if (amount > this._balance) {
            throw new InsufficientFundsError(this._balance, amount);
        }
        this._balance = this._balance - amount;
    }
}

// 处理函数
function handleAccountOperation(account: BankAccount, operation: string, amount: number): void {
    try {
        if (operation === "deposit") {
            account.deposit(amount);
            console.log(`存款 ${amount} 成功,当前余额:${account.balance}`);
        } else if (operation === "withdraw") {
            account.withdraw(amount);
            console.log(`取款 ${amount} 成功,当前余额:${account.balance}`);
        }
    } catch (e) {
        if (e instanceof InsufficientFundsError) {
            console.log(`余额不足,当前:${e.currentBalance},需要:${e.requiredAmount}`);
        } else if (e instanceof InvalidAmountError) {
            console.log(`金额无效:${e.amount}`);
        } else if (e instanceof AccountFrozenError) {
            console.log(`账户冻结:${e.reason}`);
        } else {
            console.log(`未知错误:${String(e)}`);
        }
    }
}

// 测试
let account: BankAccount = new BankAccount();
account.deposit(1000);

handleAccountOperation(account, "withdraw", 500);   // 成功
handleAccountOperation(account, "withdraw", 1000);  // 余额不足
handleAccountOperation(account, "withdraw", -100);  // 无效金额

account.freeze("涉嫌欺诈");
handleAccountOperation(account, "withdraw", 100);   // 账户冻结

练习6:编程题 - 异常包装与链

实现异常包装,保留原始异常信息:

  1. 定义 DataAccessError 异常,有属性 cause: Error 表示原始异常
  2. 定义 ServiceError 异常,有属性 cause: Error 表示原始异常
  3. 模拟三层调用:
    • queryDatabase():可能抛出 DatabaseConnectionError
    • fetchUser():调用 queryDatabase,捕获后包装为 DataAccessError
    • getUserProfile():调用 fetchUser,捕获后包装为 ServiceError
  4. 最终处理时,能够获取完整的异常链信息
练习6答案
// 底层异常
class DatabaseConnectionError extends Error {
    constructor(message: string) {
        super(message);
        this.name = "DatabaseConnectionError";
    }
}

// 包装异常
class DataAccessError extends Error {
    constructor(
        message: string,
        public readonly cause: Error
    ) {
        super(message);
        this.name = "DataAccessError";
    }
}

class ServiceError extends Error {
    constructor(
        message: string,
        public readonly cause: Error
    ) {
        super(message);
        this.name = "ServiceError";
    }
}

// 模拟三层调用
function queryDatabase(): void {
    throw new DatabaseConnectionError("连接超时");
}

function fetchUser(userId: number): void {
    try {
        queryDatabase();
    } catch (e) {
        if (e instanceof Error) {
            throw new DataAccessError(`获取用户 ${userId} 失败`, e);
        }
        throw e;
    }
}

function getUserProfile(userId: number): void {
    try {
        fetchUser(userId);
    } catch (e) {
        if (e instanceof Error) {
            throw new ServiceError("获取用户资料失败", e);
        }
        throw e;
    }
}

// 处理并打印异常链
function printErrorChain(e: Error, level: number = 0): void {
    let indent: string = "";
    for (let i = 0; i < level; i++) {
        indent = indent + "  ";
    }
    
    console.log(`${indent}[${e.name}] ${e.message}`);
    
    // 检查是否有 cause 属性
    let cause = (e as ServiceError | DataAccessError).cause;
    if (cause !== undefined && cause instanceof Error) {
        printErrorChain(cause, level + 1);
    }
}

// 测试
try {
    getUserProfile(123);
} catch (e) {
    if (e instanceof Error) {
        printErrorChain(e);
    }
}

// 输出:
// [ServiceError] 获取用户资料失败
//   [DataAccessError] 获取用户 123 失败
//     [DatabaseConnectionError] 连接超时

练习7:编程题 - 防御性编程实践

实现一个 SafeArrayUtils 工具类,包含以下方法,每个方法都要进行参数校验:

  1. getElement<T>(arr: T[], index: number): T —— 获取元素,检查索引范围
  2. setElement<T>(arr: T[], index: number, value: T): void —— 设置元素
  3. slice<T>(arr: T[], start: number, end: number): T[] —— 切片,检查参数
  4. findIndex<T>(arr: T[], predicate: (item: T) => boolean): number —— 查找索引

每个方法都要抛出有意义的异常。

练习7答案
// 工具异常
class ArrayIndexError extends Error {
    constructor(index: number, length: number) {
        super(`索引 ${index} 超出范围 [0, ${length})`);
        this.name = "ArrayIndexError";
    }
}

class ArrayArgumentError extends Error {
    constructor(message: string) {
        super(message);
        this.name = "ArrayArgumentError";
    }
}

// 安全的数组工具类
class SafeArrayUtils {
    static getElement<T>(arr: T[], index: number): T {
        // 检查数组
        if (arr === null || arr === undefined) {
            throw new ArrayArgumentError("数组不能为 null 或 undefined");
        }
        // 检查索引类型
        if (typeof index !== "number" || isNaN(index)) {
            throw new ArrayArgumentError(`索引必须是有效数字:${index}`);
        }
        // 检查索引范围
        if (index < 0 || index >= arr.length) {
            throw new ArrayIndexError(index, arr.length);
        }
        return arr[index];
    }
    
    static setElement<T>(arr: T[], index: number, value: T): void {
        if (arr === null || arr === undefined) {
            throw new ArrayArgumentError("数组不能为 null 或 undefined");
        }
        if (typeof index !== "number" || isNaN(index)) {
            throw new ArrayArgumentError(`索引必须是有效数字:${index}`);
        }
        if (index < 0 || index >= arr.length) {
            throw new ArrayIndexError(index, arr.length);
        }
        arr[index] = value;
    }
    
    static slice<T>(arr: T[], start: number, end: number): T[] {
        if (arr === null || arr === undefined) {
            throw new ArrayArgumentError("数组不能为 null 或 undefined");
        }
        if (typeof start !== "number" || isNaN(start)) {
            throw new ArrayArgumentError(`start 必须是有效数字:${start}`);
        }
        if (typeof end !== "number" || isNaN(end)) {
            throw new ArrayArgumentError(`end 必须是有效数字:${end}`);
        }
        if (start < 0 || start > arr.length) {
            throw new ArrayArgumentError(`start ${start} 超出范围 [0, ${arr.length}]`);
        }
        if (end < start || end > arr.length) {
            throw new ArrayArgumentError(`end ${end} 超出范围 [${start}, ${arr.length}]`);
        }
        
        let result: T[] = [];
        for (let i = start; i < end; i++) {
            result.push(arr[i]);
        }
        return result;
    }
    
    static findIndex<T>(arr: T[], predicate: (item: T) => boolean): number {
        if (arr === null || arr === undefined) {
            throw new ArrayArgumentError("数组不能为 null 或 undefined");
        }
        if (typeof predicate !== "function") {
            throw new ArrayArgumentError("predicate 必须是函数");
        }
        
        for (let i = 0; i < arr.length; i++) {
            if (predicate(arr[i])) {
                return i;
            }
        }
        return -1;
    }
}

// 测试
let arr: number[] = [10, 20, 30, 40, 50];

console.log(`getElement(2): ${SafeArrayUtils.getElement(arr, 2)}`);  // 30

SafeArrayUtils.setElement(arr, 1, 25);
console.log(`setElement 后: ${arr}`);  // [10, 25, 30, 40, 50]

let sliced: number[] = SafeArrayUtils.slice(arr, 1, 4);
console.log(`slice(1,4): ${sliced}`);  // [25, 30, 40]

let found: number = SafeArrayUtils.findIndex(arr, (item: number): boolean => item === 30);
console.log(`findIndex(30): ${found}`);  // 2

// 测试异常
try {
    SafeArrayUtils.getElement(arr, 10);
} catch (e) {
    console.log(`错误:${String(e)}`);
}

练习8:综合题 - 实现带异常处理的配置管理器

实现一个完整的配置管理器,包含:

  1. 异常体系:

    • ConfigError(基类)
    • ConfigFileNotFoundError(文件不存在)
    • ConfigParseError(解析失败,属性:lineNumber)
    • ConfigValidationError(验证失败,属性:field, expectedValue)
  2. ConfigManager 类:

    • 私有属性 config: Map<string, string>
    • 方法 loadFromFile(path: string): void —— 加载配置
    • 方法 get(key: string): string —— 获取值,不存在抛出异常
    • 方法 getInt(key: string): number —— 获取整数值,解析失败抛出异常
    • 方法 validateRequired(keys: string[]): void —— 验证必需键
  3. 使用 try/catch/finally 调用,finally 中记录日志

练习8答案
// 异常体系
class ConfigError extends Error {
    constructor(message: string) {
        super(message);
        this.name = "ConfigError";
    }
}

class ConfigFileNotFoundError extends ConfigError {
    constructor(public readonly path: string) {
        super(`配置文件不存在:${path}`);
        this.name = "ConfigFileNotFoundError";
    }
}

class ConfigParseError extends ConfigError {
    constructor(
        message: string,
        public readonly lineNumber: number
    ) {
        super(`第 ${lineNumber} 行解析错误:${message}`);
        this.name = "ConfigParseError";
    }
}

class ConfigValidationError extends ConfigError {
    constructor(
        message: string,
        public readonly field: string,
        public readonly expectedValue: string
    ) {
        super(`${field} 验证失败:${message},期望值:${expectedValue}`);
        this.name = "ConfigValidationError";
    }
}

class ConfigKeyNotFoundError extends ConfigError {
    constructor(public readonly key: string) {
        super(`配置项不存在:${key}`);
        this.name = "ConfigKeyNotFoundError";
    }
}

// 配置管理器
class ConfigManager {
    private config: Map<string, string> = new Map<string, string>();
    
    loadFromFile(path: string): void {
        // 模拟文件检查
        if (path.indexOf("notfound") >= 0) {
            throw new ConfigFileNotFoundError(path);
        }
        
        // 模拟解析
        if (path.indexOf("parseerror") >= 0) {
            throw new ConfigParseError("格式错误", 5);
        }
        
        // 模拟加载成功
        this.config.set("host", "localhost");
        this.config.set("port", "8080");
        this.config.set("timeout", "30");
    }
    
    get(key: string): string {
        if (!this.config.has(key)) {
            throw new ConfigKeyNotFoundError(key);
        }
        return this.config.get(key) as string;
    }
    
    getInt(key: string): number {
        let value: string = this.get(key);
        let num: number = parseInt(value, 10);
        if (isNaN(num)) {
            throw new ConfigParseError(`"${value}" 不是有效整数`, 0);
        }
        return num;
    }
    
    validateRequired(keys: string[]): void {
        for (let i = 0; i < keys.length; i++) {
            let key: string = keys[i];
            if (!this.config.has(key)) {
                throw new ConfigValidationError(
                    "缺少必需配置项",
                    key,
                    "存在"
                );
            }
        }
    }
    
    set(key: string, value: string): void {
        this.config.set(key, value);
    }
}

// 使用示例
function loadConfiguration(path: string): ConfigManager | null {
    let manager: ConfigManager = new ConfigManager();
    
    try {
        console.log(`开始加载配置:${path}`);
        manager.loadFromFile(path);
        manager.validateRequired(["host", "port"]);
        
        let host: string = manager.get("host");
        let port: number = manager.getInt("port");
        console.log(`配置加载成功:${host}:${port}`);
        
        return manager;
    } catch (e) {
        if (e instanceof ConfigFileNotFoundError) {
            console.log(`配置文件缺失:${e.path}`);
        } else if (e instanceof ConfigParseError) {
            console.log(`解析错误:${e.message}`);
        } else if (e instanceof ConfigValidationError) {
            console.log(`验证失败:${e.field} ${e.message}`);
        } else if (e instanceof ConfigError) {
            console.log(`配置错误:${e.message}`);
        } else {
            console.log(`未知错误:${String(e)}`);
        }
        return null;
    } finally {
        console.log("配置加载操作完成");
    }
}

// 测试
loadConfiguration("/app/config.txt");           // 正常
loadConfiguration("/app/notfound.txt");         // 文件不存在
loadConfiguration("/app/parseerror.txt");       // 解析错误

附录:ArkTS 错误处理速查表

// 1. 抛出异常
throw new Error("错误信息");
throw new MyCustomError("错误信息", extraData);

// 2. 自定义异常
class MyError extends Error {
    constructor(message: string, public readonly code: number) {
        super(message);
        this.name = "MyError";
    }
}

// 3. 捕获异常
try {
    riskyOperation();
} catch (e) {
    if (e instanceof MyError) {
        console.log(`我的错误:${e.message},代码:${e.code}`);
    } else if (e instanceof Error) {
        console.log(`一般错误:${e.message}`);
    } else {
        console.log(`未知错误:${String(e)}`);
    }
} finally {
    // 清理资源
}

// 4. 异常包装
try {
    lowLevelOperation();
} catch (e) {
    if (e instanceof Error) {
        throw new HighLevelError("包装", e);
    }
    throw e;
}

// 5. 防御性编程
function safeDivide(a: number, b: number): number {
    if (typeof a !== "number" || typeof b !== "number") {
        throw new Error("参数必须是数字");
    }
    if (b === 0) {
        throw new Error("除数不能为零");
    }
    return a / b;
}
posted @ 2026-04-16 15:52  thammer  阅读(15)  评论(0)    收藏  举报