# Errors Public reportable Kaptan error enums derive `KaptanError`: ```rust,ignore #[derive(Debug, kaptan_err_reportable::KaptanError)] enum SensorError { #[kaptan_error(id = 12345)] TransferFault, #[kaptan_error(id = 12346)] UnexpectedIdentity(u8), } #[kaptan_err_reportable::propagate] fn service_sensor() -> Result<(), SensorError> { read_sensor()?; Ok(()) } ``` The derive implements `core::error::Error`, `Display`, and `ReportableError`. It requires a nonzero explicit ID on every variant and retains no type, variant, or module string. `Display` renders the decimal ID. Renaming, moving, or reordering a Rust variant therefore does not change its receiver contract. Missing and same-enum duplicate IDs are rejected by the derive. Reusing an ID across error enums generates the same reserved symbol. Collision detection requires both defining objects to participate in compilation or final linking. It does not inspect unextracted archive members or prove catalog-wide uniqueness. Stable ID allocation still requires review. The [final-link fixture](../../firmware/libs/kaptan-err-reportable-macros/tests/final_link.md) checks Rust 1.90.0 release fat-LTO and native Linux static-library links. Run it with `make -C firmware native-test`. Each run retains its results and Rust compiler identity in `results.txt`. `#[kaptan_err_reportable::propagate]` turns an annotated function into a reporting point. If a reportable `Err` leaves that function, the attribute makes one nonblocking admission attempt and returns the exact same `Err` to the caller. The record contains the final converted error ID and the compile-time `file:line` of that reporting point. This is where the `Err` enters diagnostics, not necessarily the line where a deeper callee originally constructed it. `?`, explicit returns, result aliases, and `From` conversion retain their normal Rust behavior. An error handled inside the function is silent. ## Stable IDs Production error IDs are plain decimal numbers from `1` through `59999`. Values from `60000` through `65535` are reserved for tests, and `0` is invalid. Once issued, an ID is never renumbered or reused, even if its error variant is removed. Adding a variant therefore requires allocating a new ID; renaming, moving, or reordering an existing variant keeps its assigned ID. The example numbers above are illustrative and must be replaced with unallocated catalog values when used in production code. Only the intended reporting point is annotated. If nested functions are both annotated, each function makes its own report attempt; the ingress does not mark or mutate error values to deduplicate them. Expected validation, backpressure, retry, and untrusted-input results remain unannotated. In particular, transient SPI/I²C `Busy` and unavailable range measurements stay inside their service retry/idle paths. In contrast, every logging macro—including `kaptan_log::error!`—produces only `LOG`. Error reporting is not a replacement for safety state, actuator inhibit, or typed configuration results. `kaptan-err-reportable` owns reportable-error identity and its independent one-slot ingress. A pending record contains only the compact ID and a static reference to the reporting point's `file:line`. The MAVLink adapter copies that location into its fixed field when constructing `ERROR`. Producers never encode or transmit MAVLink themselves. Admission is bounded and lossy if a different reportable error is already pending. ## What the receiver sees - A rejected KCFG upload receives an authoritative `FILE_CONTROL` status and bounded readable detail. Invalid user content is not a firmware error. A durable-storage failure additionally emits one stable `PersistConfigurationError` ID that distinguishes journal geometry, model, sequence, flash-controller, bounds, unlock, and verification failures. Replaying the cached close result does not repeat the report or flash operation. Any persistence error makes committed state unknown and blocks ordinary saves until reboot recovery or a successful explicit factory reset. Boot-active values remain unchanged. - A stale KCFG expected base returns `REQUEST_CONFLICT` before persistence. It is a reload-required editing conflict, not a reportable firmware error. Invalid boot configuration uses the UART-disabled USB setup path and retains readable saved values for repair. See [Configuration](configuration.md). - SPI completion errors never copy partial data into the caller's receive buffer. If bounded shutdown cannot establish quiescence, the SPI route quarantines its static DMA storage and refuses further transfers. This is distinct from transient `Busy`. Physical SPI shutdown and fault behavior remain hardware-validation work. - Reported `ReportableError` values reach the F722 MAVLink worker as a `ERROR`, subject to bounded transport admission. The payload is `{ error_id, raised_at[96] }`. `raised_at` is a zero-padded UTF-8 `file:line` prefix for the reporting point. Dynamic human-readable context belongs in `LOG`, while structured protocol failures remain in their owning response message. - Control, navigation and safety error types are derive-ready but their live owners are not yet attached to a MAVLink worker. F722 uses `panic-halt`, without live panic reporting or retained-record replay. No diagnostic message may synchronously transmit from a panic handler, hard fault, ISR, DMA callback, or motor-inhibit path. A later target integration must inhibit outputs first, persist a validated crash record, then report it only after MAVLink is available on the next boot.