# 错误处理 (/zh/docs/rust/errors)

IztroError 的四个变体、code() 分类标识、BridgeError 与排查方式。



所有收外部输入的入口都返回 `Result`。日期格式、日期存在性、年份范围、
时辰索引与反推入参都在核心层前置校验，非法输入得到错误值而不是 panic。

```rust
#[non_exhaustive]
pub enum IztroError {
    /// 日期串格式错、日期不存在，或超出公历 1583–9999
    InvalidDate(String),
    /// 时辰索引超出 0–12
    InvalidTimeIndex(u8),
    /// 反推入参不合法：干支阴阳不配、条件为空或含流耀、年份范围非法
    InvalidArgument(String),
    /// 依赖的历法库没给出本应存在的干支或星座取值
    Internal(String),
}

impl IztroError {
    /// 机器可读的错误分类标识
    pub fn code(&self) -> &'static str
}
```

`IztroError` 实现了 `Display` 与 `std::error::Error`，可直接用 `?` 传播、用 `{}` 打印。

## code() [#code]

`Display` 给的是面向人的文案，会随版本调整；要在程序里分支请用 `code()`：

| 变体                 | `code()`             | 含义          |
| ------------------ | -------------------- | ----------- |
| `InvalidDate`      | `invalid_date`       | 日期非法        |
| `InvalidTimeIndex` | `invalid_time_index` | 时辰索引越界      |
| `InvalidArgument`  | `invalid_argument`   | 反推等入口的入参不合法 |
| `Internal`         | `internal`           | 库内部缺陷       |

这四个取值与 Python 的 `IztroError.code`、Go 的 `iztro.Error.Code` 是同一套，
跨语言分支逻辑可以照抄。

```rust
let e = by_solar("2000-2-30", 2, Gender::Female, true, Language::ZhCN, Config::default())
    .unwrap_err();
println!("{} / {}", e.code(), e);
```

**输出**

```text
invalid_date / invalid solar date '2000-2-30': day is out of range for that month
```

<Callout type="warn" title="枚举是 #[non_exhaustive] 的">
  `IztroError` 标了 `#[non_exhaustive]`，crate 外无法穷尽匹配——`match` 必须带兜底分支。
  将来新增错误分类因此不构成破坏性变更；按 `code()` 分支的代码则完全不受影响。
</Callout>

***

## InvalidDate [#invaliddate]

**触发条件**

| 情形              | 例             | 消息                                   |
| --------------- | ------------- | ------------------------------------ |
| 格式不是 `YYYY-M-D` | `"2000/8/16"` | `expected 'YYYY-M-D'`                |
| 年、月、日不是数字       | `"abc-8-16"`  | `year is not a number`               |
| 月份越界            | `"2000-13-1"` | `month must be within 1-12`          |
| 该月没有这一天         | `"2000-2-30"` | `day is out of range for that month` |
| 年份超出支持范围        | `"1500-1-1"`  | `year must be within 1583-9999`      |

农历专有（只有 `by_lunar` 与 `get_sign_by_lunar_date`、`get_major_star_by_lunar_date` 会碰到）：

| 情形        | 例                                  | 消息                                         |
| --------- | ---------------------------------- | ------------------------------------------ |
| 该农历年没有这个月 | `by_lunar("2000-13-1", …)` 之外的表内缺月 | `month does not exist in that lunar year`  |
| 该农历月没有这一天 | `"2000-7-30"`（七月是小月）               | `day is out of range for that lunar month` |

农历消息的前缀是 `invalid lunar date '<原串>': `，公历是 `invalid solar date '<原串>': `，
从消息就能看出走的是哪个入口。

**示例**

```rust
let cfg = Config::default();

for date in ["2000-13-1", "2000-2-30", "1500-1-1"] {
    match by_solar(date, 2, Gender::Female, true, Language::ZhCN, cfg.clone()) {
        Ok(_) => println!("{date}: ok"),
        Err(e) => println!("{e}"),
    }
}
```

**输出**

```text
invalid solar date '2000-13-1': month must be within 1-12
invalid solar date '2000-2-30': day is out of range for that month
invalid solar date '1500-1-1': year must be within 1583-9999
```

消息里带上了原始输入，便于在批量处理时定位是哪一条数据出的问题。

**边界与陷阱**

<Accordions>
  <Accordion title="年份下限 1583 的由来">
    1582 年格里历改革当年有一段不存在的日期。底层历法库在这些日期上没有定义，
    因此支持范围从改革完成后的 1583 年起算。上限 9999 是农历数据表的覆盖终点。
  </Accordion>

  <Accordion title="月日不必补零">
    `"2000-8-16"` 与 `"2000-08-16"` 都接受。分隔符必须是 `-`。
  </Accordion>

  <Accordion title="农历闰月的校验">
    `by_lunar` 会检查该农历年该月是否真的存在，以及该月有多少天（大月 30、小月 29）。
    `leap` 标为闰月但那年那月无闰月时不报错，按普通月处理。
  </Accordion>
</Accordions>

***

## InvalidTimeIndex [#invalidtimeindex]

**触发条件**　时辰索引大于 12。

**示例**

```rust
let err = by_solar("2000-8-16", 13, Gender::Female, true, Language::ZhCN, Config::default())
    .unwrap_err();
println!("{err}");
```

**输出**

```text
time_index must be 0-12, got 13
```

**边界与陷阱**

<Callout type="info" title="13 个而不是 12 个">
  子时跨午夜，拆成早子时（索引 0）与晚子时（索引 12），因此合法值有 13 个。
  从小时数换算用 [`time_to_index`](/zh/docs/rust/util#time_to_index)，它保证结果落在合法范围。
</Callout>

***

## InvalidArgument [#invalidargument]

**触发条件**　反推入口的调用方错误：

| 情形       | 例                            | 消息                                                                            |
| -------- | ---------------------------- | ----------------------------------------------------------------------------- |
| 四柱干支阴阳不配 | 甲丑（阳干配阴支）                    | `invalid yearly pillar: stem and branch must have the same polarity`          |
| 反推条件为空   | `ReverseCriteria::default()` | `reverse criteria must contain at least one condition`                        |
| 条件含运限流曜  | 流禄                           | `star 'liulu' is a horoscope-scope star and never appears on the natal chart` |
| 年份范围非法   | `(2100, 1900)`               | `invalid year range 2100-1900: expected 1583-9999 with start <= end`          |

**示例**

```rust
let jia_zi = (HeavenlyStem::Jia, EarthlyBranch::Zi);
let err = solar_dates_by_bazi(
    (HeavenlyStem::Jia, EarthlyBranch::Chou), // 甲丑：阴阳不配
    jia_zi, jia_zi, jia_zi,
    (1900, 2100),
    &Config::default(),
)
.unwrap_err();
println!("{} / {}", err.code(), err);
```

**输出**

```text
invalid_argument / invalid yearly pillar: stem and branch must have the same polarity
```

反推入口的语义见[反推](/zh/docs/rust/reverse)。

***

## Internal [#internal]

**触发条件**　依赖的历法库 `lunar_rust` 没有交回本应存在的干支或星座取值。
这是库内部缺陷而非调用方过错。

**为什么不 panic**　wasm 目标下 panic 即 trap，而且每次 trap 都会永久损耗模块实例的
栈空间。把这类情况落成错误值，非法调用就不会累积损坏 Go 侧的 wasm 实例。

**消息形状**　`internal error: <细节>`，`code()` 为 `internal`。

<Callout type="warn">
  在金标覆盖的全部日期上都没有触发过这个变体。真的碰到请当作 bug 上报，
  并附上完整的排盘入参。
</Callout>

***

## BridgeError [#bridgeerror]

绑定层（C FFI / wasm / PyO3）对外报错的统一形状。Rust 调用方一般用不到它——
它是 Python 与 Go 侧错误对象的来源。

```rust
pub struct BridgeError {
    /// invalid_date / invalid_time_index / invalid_argument / internal
    pub code: &'static str,
    /// 面向人的错误描述
    pub message: String,
}

impl BridgeError {
    pub fn invalid_argument(message: impl Into<String>) -> Self
    pub fn internal(message: impl Into<String>) -> Self
}

impl From<IztroError> for BridgeError { /* code 与 message 直接沿用 */ }
```

四个分类与 `IztroError` 相同。绑定层收的是字符串而非枚举，
性别、语言、宫名、四化、配置 JSON 这些取值的合法性只能在那一层校验，
同样落在 `invalid_argument` 分类下。

三条出口都把它序列化成同一段 JSON：

```json
{ "error": "invalid solar date '2000-2-30': day is out of range for that month",
  "code": "invalid_date" }
```

Python 侧变成 `IztroError`（继承 `ValueError`，带 `.code`），
Go 侧变成 `*iztro.Error`（带 `Code`、可用 `errors.Is` 比对哨兵）。

## C FFI [#c-ffi]

`x_iztro::ffi` 导出的 C ABI 里，统一查询入口是 `iztro_query`：

```c
// include/x_iztro.h
char *iztro_query(const char *query_json);
void  iztro_free_string(char *s);
```

收一段以 `kind` 选择具体查询的 JSON（如 `{"kind":"getPalaceNames","soulIndex":0}`），
成功返回 `{"value": <结果>}`，失败返回上面那段带 `code` 的错误 JSON。
它把轻量查询、宫位推算、工具函数、安星、数据表、翻译与 Prompt 生成收进一个符号，
不必为每个函数导出一个 C 符号。键名 camelCase，星耀、干支、宫位等标识按
iztro i18n key 传入与返回。

返回的字符串都由调用方用 `iztro_free_string` 归还；这些函数永不返回 `NULL`。

<Callout type="info">
  `ffi` 模块标了 `#[doc(hidden)]`，不是给 Rust 调用方用的——
  Rust 里直接调 `by_solar` 一族即可。
</Callout>

***

## 处理方式 [#处理方式]

**用 `?` 传播**

```rust
fn analyze(date: &str) -> Result<String, IztroError> {
    let chart = by_solar(date, 2, Gender::Female, true, Language::ZhCN, Config::default())?;
    Ok(chart.palace(Palace::Soul).unwrap().major_stars
        .iter().map(|s| s.name.clone()).collect::<Vec<_>>().join(","))
}
```

**分变体处理**

```rust
fn describe(date: &str, ti: u8) -> String {
    match by_solar(date, ti, Gender::Female, true, Language::ZhCN, Config::default()) {
        Ok(chart) => format!("排盘成功: {}", chart.solar_date),
        Err(IztroError::InvalidDate(msg)) => format!("日期有误: {msg}"),
        Err(IztroError::InvalidTimeIndex(t)) => format!("时辰索引 {t} 越界"),
        Err(e) => format!("其他错误 [{}]: {e}", e.code()),
    }
}

println!("{}", describe("2000-8-16", 2));
println!("{}", describe("2000-2-30", 2));
println!("{}", describe("2000-8-16", 13));
```

**输出**

```text
排盘成功: 2000-8-16
日期有误: invalid solar date '2000-2-30': day is out of range for that month
时辰索引 13 越界
```

<Callout type="warn" title="必须留通配分支">
  `IztroError` 标了 `#[non_exhaustive]`，crate 外的 `match` 必须带一个
  `_` 或 `Err(e)` 兜底分支，否则编译不过。这样将来新增变体不会成为破坏性变更。
</Callout>

**转成自己的错误类型**

`IztroError` 实现了 `std::error::Error`，可直接被 `Box<dyn Error>`、`anyhow::Error`
或 `thiserror` 的 `#[from]` 接住。

```rust
#[derive(Debug, thiserror::Error)]
enum AppError {
    #[error("排盘失败: {0}")]
    Chart(#[from] x_iztro::IztroError),
}
```

***

## 关于 panic [#关于-panic]

排盘入口不会因非法**外部输入**而 panic：日期、时辰这些都落成 `IztroError`，
连历法库交不出取值这种内部情形也走 `IztroError::Internal` 而不是 panic。
仍可能 panic 的只有库自身的逻辑缺陷（如断言失败），这类情况应视为 bug 上报。

<Callout type="warn" title="wasm 目标下尤其重要">
  wasm 上 panic 即 trap，而且每次 trap 都会永久损耗模块实例的栈空间。
  因此校验放在核心层而非绑定层——三种语言共用同一道防线。
</Callout>
