ADR-00017: FFI error detail on the Complete event¶
| Field | Value |
|---|---|
| Status | accepted |
| Date | 2026-06-18 |
| Deciders | Raghav Narula |
Context and Problem Statement¶
The C API reported a failure in two limited ways. A failing operation sent a mid-stream LORE_EVENT_ERROR event. That event held a translated LoreError code and a message. The Complete event held only a flat status: 0 for success or 1 for failure. The synchronous functions returned the same flat 0 or 1. For most errors the translated LoreError code did not match the error's real FFI code. A consumer could not tell where an error came from without reading server logs.
A consumer needs one reliable way to read a failure. One event should carry the real error code, the message, and the captured trace. The consumer should not need server logs.
Decision Drivers¶
- A consumer should read the full error detail from one event.
- The code a consumer reads should be the real FFI code, not a translation that drops detail.
- The synchronous return value and the event status should never differ.
- A consumer should rebuild the file, line, column, and context of a failure without server logs.
- The layout change should only add fields, so old field reads keep working after a recompile.
Considered Options¶
- Enrich the terminal
Completeevent and stop emittingErroron a terminal failure - Enrich the mid-stream
Errorevent - Enrich both the
ErrorandCompleteevents - Add a formatted trace string instead of structured trace locations
- Widen the
EventErrortrait to carry the code, message, and trace
Decision Outcome¶
Chosen option: "Enrich the terminal Complete event and stop emitting Error on a terminal failure." It gives a consumer one error-bearing event with the full structured detail. The synchronous return value and Complete.status always match. The layout only gains new fields.
The decision has three parts:
- Put the full error detail on the terminal
Completeevent as a structuredLoreErrorDetail: the code, the message, and aLoreArray<LoreTraceLocation>. - Use the error's FFI code everywhere. On failure,
status, the synchronous return value, anderror.error_codeall hold that code. On success they hold0. - Stop emitting the mid-stream
LORE_EVENT_ERRORevent on a terminal failure.
Consequences¶
- Good, because a failing operation sends one error-bearing event, the enriched
Complete, and a consumer reads the code, message, and trace from it. - Good, because the synchronous return value and
Complete.statusalways match. Both come from the same outcome. - Good, because the code is the real FFI code, not the
LoreErrortranslation that drops detail. - Good, because the layout only gains fields, so a consumer that reads just the old fields still compiles after a recompile.
- Bad, because this changes the meaning of a documented contract. A consumer that branched on the old
{0, 1}status must change. So must one that waited for anErrorevent on a terminal failure. - Bad, because adding a field to the
Completepayload grows the event union that holds it, so every consumer must recompile. The change is source-compatible, not binary-compatible. - Neutral, because the
LoreErrorenum, theerror_typefield, and theLORE_EVENT_ERRORvariant stay defined. The library only stops sendingLORE_EVENT_ERRORon a terminal failure.
Pros and Cons of the Options¶
Enrich the terminal Complete event and stop emitting Error on a terminal failure¶
- Good, because there is one error-bearing event, so it is clear which event carries the detail.
- Good, because
Completealways fires. An async consumer cannot read a return value, but it still gets the code and detail. - Good, because the change only adds fields and stays source-compatible.
- Bad, because consumers that read the mid-stream
Errorevent must move to readingComplete.
Enrich the mid-stream Error event¶
- Good, because the
Errorevent already exists for failures. - Bad, because
Errordoes not fire on every path. A consumer would still needCompletefor the status. That leaves two channels. - Bad, because the synchronous return value and the
Errorevent could still differ.
Enrich both the Error and Complete events¶
- Good, because every existing consumer keeps working unchanged.
- Bad, because it carries the same detail twice, with two chances to differ.
- Bad, because it keeps two error channels instead of one.
A formatted trace string instead of structured trace locations¶
- Good, because a single string is simple to carry.
- Bad, because a consumer cannot read the file, line, column, and context as fields. It would have to parse a string whose format is not a contract.
Widen the EventError trait to carry the code, message, and trace¶
- Good, because it would reuse the existing error type.
- Bad, because many types implement the trait, so widening it changes every one of them.
- Bad, because reading the trace from the concrete error before it widens is cheaper. A small
HasTracetrait covers the generic case and leavesEventErrorunchanged.
More Information¶
This ADR originally closed by saying the code values were not yet stable to branch on, that a table mapping each code to a name would ship in later work, and that error_code should be treated as opaque until then. That work has since landed, so the caveat no longer applies.
lore-base/src/error.rs is the registry. Every code is allocated from a block that groups errors by the kind of failure they report, so a consumer can branch on a block range when it only cares about the category, or on an exact value when it cares about the specific error. The allocation rules forbid renumbering an existing code to close a gap, so a value a consumer branches on does not move under it; a new error type takes a free code from its group's block.
Delivering the table reallocated the values once. A consumer pinned to a code from before that work must update — the numbering in use when this ADR was written is not the numbering that ships now.
Codes are also process exit statuses: the CLI returns the failing error's code from main as ExitCode::from(code as u8). The allocation therefore keeps every block clear of the values the shell and the OS already spend on something else, so a code can never be confused with a signal death or a shell error.
See the FFI error reporting contract and the allocation table in docs/developing/code-standards/errors.md.