Guides

Error handling

What is validated, at which layer, what the four error categories mean, and why the core refuses to panic.

For: developers

x-iztro validates external input as far in as it can, so all three programming languages sit behind one line of defence. Each language only translates the error into its own conventional type; none of them re-validates or reinterprets.

Error categories

Every error carries a machine-readable category, with the same values across languages — branch on it, do not parse the message.

CategoryMeaning
invalid_dateMalformed date, a date that does not exist, or one outside the supported range (Gregorian 1583–9999)
invalid_time_indexHour index out of range (0–12 are legal)
invalid_argumentAny other invalid argument or config: unknown gender, chart language, star key, switch value, a wrong override-table length; or reverse-lookup input — mismatched pillar polarity, empty or flow-star criteria, a bad year range
internalA defect or runtime failure inside the library. Not the caller's fault; please report it

The error type in each language

The IztroError enum, with code() giving the category:

match by_solar(date, ti, Gender::Female, true, Language::EnUS, Config::default()) {
    Ok(chart) => { /* ... */ }
    Err(e) => println!("{:20} {}", e.code(), e),
}
invalid_date         invalid solar date '2000-2-30': day is out of range for that month
invalid_date         invalid solar date '1000-1-1': year must be within 1583-9999
invalid_time_index   time_index must be 0-12, got 13

There are four variants: InvalidDate, InvalidTimeIndex, InvalidArgument and Internal. The enum is marked #[non_exhaustive], so leave a _ arm when matching.

The C FFI and wasm exits render the same error as {"error":"<message>","code":"<code>"}, generated by serde so escaping is always complete.

What is validated, with sample messages

Messages start lower-case, introduce the detail after a colon, and carry the original input — so batch processing can pinpoint which record went wrong.

InputSample message
Gregorian date formatinvalid solar date 'not-a-date': year is not a number
Gregorian date does not existinvalid solar date '2000-2-30': day is out of range for that month
Gregorian year rangeinvalid solar date '1000-1-1': year must be within 1583-9999
Lunar monthinvalid lunar date '2000-13-1': month must be within 1-12
Days in that lunar monthinvalid lunar date '2000-2-31': day is out of range for that lunar month
Hour indextime_index must be 0-12, got 13
Genderinvalid gender 'x': expected 'male' or 'female'
Chart languageinvalid language 'fr-FR': expected one of zh-CN, zh-TW, en-US, ja-JP, ko-KR, vi-VN
Custom mutagen table lengthinvalid mutagens for 'gengHeavenly': expected 4 stars (lu, quan, ke, ji), got 3
Override table given a translated nameinvalid mutagens for 'gengHeavenly': unknown star '太阳'

Which layer validates what

Dates and hour indexes are validated in the core, identically for all three programming languages. Gender, chart language, configuration switches and star keys — the things passed as strings — are validated in the binding layer as they are parsed; on the Rust side they are enums to begin with, so there is no invalid value to reject.

A miss is not an error

Entry points that compute return errors; query methods return an empty value on a miss rather than an error — "this chart does not have that star" is a normal result, not an exception.

SituationReturns
A star is not on this chartNone / nil
Palace index out of rangeNone / nil
The star has no brightness tableNone / empty string
Reverse lookup of a name that does not existNone / empty string

A misspelled key fails silently

chart.palace("soulPalce") raises nothing and simply returns empty; has(["ziweiMj"]) returns False forever. Use enums or constants in predicates (Python's PalaceName.SOUL, Go's iztro.PalaceSoul) — then a misspelling is a compile-time or construction-time error, not a silent runtime one. To validate a string that came from outside, feed it to the enum constructor: PalaceName("x") raises ValueError.

Why the core does not panic

This is not a style preference; it is a hard constraint imposed by the wasm target.

On wasm a panic becomes a trap, aborting the call outright
catch_unwind does not work on wasm — the binding layer cannot catch it
Every trap permanently consumes stack space in the module instance, and once enough have accumulated even legitimate calls start failing

So the line of defence has to sit further in: all external input is validated before it reaches the algorithm, and the entry points return a Result. The binding layer's catch_unwind is a backstop for defects inside the library only; it carries no argument-validation duty.

What a panic would mean

The charting entry points do not panic on invalid external input. If you hit one anyway, it is a defect inside the library; it is returned under the internal category and should be reported as a bug — not something callers are expected to defend against.

Writing a batch job

Skip the bad records, keep going with the good ones, rather than failing the whole batch:

let (charts, failed): (Vec<_>, Vec<_>) = rows
    .iter()
    .map(|r| by_solar(&r.date, r.ti, r.gender, true, Language::EnUS, Config::default()))
    .partition(Result::is_ok);

Per-API error behaviour is on the errors page for Rust, Python and Go.

On this page