# 错误处理分层设计 Skill

## 目标

设计错误处理时，底层通用设施应尽可能开放，不替调用者决定错误处理方式；高层业务接口应收束语义，避免暴露过多错误处理范式，降低使用成本和实现复杂度。

## 核心原则

### 1. 底层通用函数：提供错误处理范式，不限制调用者

底层函数、宏、工具类、适配层应尽可能保留完整错误信息，并支持不同调用风格，例如：

* 返回错误码
* 返回 `expected<T, E>`
* 抛出异常
* 回调式错误处理
* 日志记录后继续传播
* 捕获异常并转换为统一错误对象

底层的职责不是判断哪种方式“最好”，而是提供足够通用的错误表达能力，使不同调用者可以根据自己的上下文选择合适方式。

底层设计应优先关注：

* 错误信息完整
* 错误来源明确
* 可组合
* 可转换
* 不丢失上下文
* 不强制调用者使用某一种错误处理模型

示例原则：

```cpp
// 底层可以提供统一的错误对象
struct error_info {
    int code {};
    std::string message;
    std::string file;
    int line {};
    std::string function;
};

// 底层提供构造错误的统一入口
#define MAKE_ERROR(code, msg) \
    error_info{code, msg, __FILE__, __LINE__, __func__}
```

底层可以围绕这个统一错误对象，再提供不同适配形式：

```cpp
// expected 风格
expected<T, error_info>

// 异常风格
throw project_exception(error_info)

// 错误码风格
return error_info

// 回调风格
on_error(error_info)
```

### 2. 高层函数：根据语义选择一种主要错误模型

高层函数不应该为了“灵活”而同时暴露多种错误处理接口。

错误示例：

```cpp
foo();
foo_expected();
foo_nothrow();
foo_with_error_code();
```

除非确实有强需求，否则这种接口会增加维护成本，也会让调用者不知道应该使用哪个。

高层函数应根据语义选择一种主要错误模型：

#### 简单高层函数

简单操作可以直接使用异常。

适合场景：

* 调用失败通常不是正常业务分支
* 调用者一般无法局部恢复
* 失败代表流程应中断
* 接口希望保持简洁

```cpp
User load_user(UserId id); // 找不到、数据库异常等直接抛异常
```

#### 复杂高层函数

复杂业务流程可以使用 `expected<T, E>` 表示预期内的失败结果。

适合场景：

* 失败是业务的一部分
* 调用者需要根据失败类型分支处理
* 成功和失败都是接口语义的一部分
* 不希望用异常表达正常业务分支

```cpp
expected<Order, CreateOrderError> create_order(CreateOrderRequest req);
```

例如：

* 参数校验失败
* 库存不足
* 权限不足
* 用户状态不允许
* 资源不存在

这些属于“预期的失败结果”，适合用 `expected`。

### 3. 异常表示错误，但不等于错误处理

异常是一种错误传播机制，不是完整的错误处理策略。

异常适合表达：

* 不可继续的错误
* 非预期错误
* 系统错误
* 编程错误
* 资源错误
* 调用者当前层级无法处理的问题

但异常本身不负责：

* 是否重试
* 是否降级
* 是否返回默认值
* 是否记录日志
* 是否转换成 HTTP 响应
* 是否转换成 UI 错误提示

这些应由更高层的边界处理逻辑完成。

### 4. expected 表示预期结果，异常表示非预期错误

推荐约定：

```text
expected<T, E>：用于表达业务上可预期、调用者应当处理的失败
exception：用于表达非预期、当前层级无法合理处理的错误
```

例如：

```cpp
expected<User, LoginError> login(name, password);
```

登录失败是业务结果，所以适合 `expected`。

```cpp
Config load_config(path);
```

配置文件损坏、磁盘读取失败、解析器内部异常等，可以直接抛异常，因为这通常不是普通业务分支。

### 5. 边界层负责错误收束

在模块边界、线程边界、HTTP 边界、RPC 边界、插件边界，应捕获并转换错误。

例如 HTTP 层：

```cpp
try {
    auto result = service.do_work(req);

    if (!result) {
        return make_business_error_response(result.error());
    }

    return make_success_response(*result);
}
catch (const project_exception& e) {
    return make_internal_error_response(e.info());
}
catch (const std::exception& e) {
    return make_unknown_error_response(e.what());
}
catch (...) {
    return make_unknown_error_response("unknown exception");
}
```

边界层的职责是：

* 捕获异常
* 统一日志
* 统一响应格式
* 避免异常逃逸到框架之外
* 把内部错误转换成外部协议错误

### 6. 不要在每一层都处理错误

中间层如果没有恢复能力，不应该假装处理错误。

错误示例：

```cpp
try {
    return lower_call();
}
catch (...) {
    log_error();
    throw;
}
```

如果只是重复日志、重复包装、重复捕获，可能导致：

* 日志重复
* 栈信息污染
* 错误语义变模糊
* 维护成本增加

中间层只有在以下情况才应该处理错误：

* 能恢复
* 能补充关键上下文
* 能转换错误语义
* 能降级
* 能重试
* 到达了模块边界

### 7. 推荐分层模型

```text
底层工具层：
    提供统一错误对象、宏、异常类型、expected 适配、错误码适配

基础库层：
    尽量保留错误上下文，不强制业务语义

领域服务层：
    根据业务语义选择 expected 或 exception

接口边界层：
    捕获异常，处理 expected，转换成 HTTP/RPC/UI/日志格式
```

### 8. 总结规则

底层：

```text
越底层，越通用，越不替调用者做决定。
```

高层：

```text
越高层，越具体，越应该收束错误语义。
```

简单高层函数：

```text
可以直接异常。
```

复杂业务函数：

```text
优先 expected 表达预期失败。
```

异常：

```text
表示错误传播，不等于错误处理。
```

边界层：

```text
负责最终捕获、记录、转换、响应。
```
