Rust Result and C++ Exceptions: Error Contracts¶
Rust Result and C++ exceptions can both report a failed operation, but they expose different interfaces and control flow. Start with the failure contract, not a mechanical replacement of throw with panic. This lesson compares a strict integer parser and the cleanup surrounding each attempt.
Rust and C++ parsing requirements¶
Read Option and Result and scope cleanup first. The independent fixtures require Rust edition 2024 and a C++20 compiler, with no external packages or sensors.
Accept an optional ASCII minus followed by at least one ASCII digit. Leading zeros and negative zero are allowed. Reject plus signs, spaces, trailing characters and non-ASCII digits as Format. A syntactically valid integer outside signed 32-bit range is Range. Syntax validation takes precedence: even a very long number followed by a letter is Format.
For each attempted input, create a small guard and close it exactly once when returning normally or reporting a parsing failure. A counter models cleanup; it does not acquire a real device. Continue after bad inputs and preserve zero as a valid reading. Both implementations must produce:
C++: typed exceptions and unwinding¶
Save this complete program as main.cpp. from_chars itself reports an error code; this adapter deliberately turns it into a typed exception. The catch is inside the loop, so one malformed input does not stop the report.
Run with assertions enabled:
The return type alone does not list possible exceptions. Document ParseError and any other failure sources. During propagation to this handler, the constructed automatic guard is destroyed; that is exception unwinding, not a normal return. The C++ destruction rules govern that cleanup. The current working draft contains later additions too; this fixture uses C++20 features.
The from_chars contract includes partial consumption and range errors. Checking both syntax and the returned pointer prevents silently accepting a prefix. Other parsers may accept whitespace or a plus sign: substituting one requires retesting the same contract.
Rust: a typed failure value, not a panic¶
Create a project with cargo new error_contract --edition 2024. Replace src/main.rs with the entire program below. Error is a local domain type; standard-library parse errors are translated at the boundary.
Inside the project, run:
The Result API represents success or failure as a value. The ? operator here returns an Err early; it does not initiate panic unwinding. On either return path, the local guard is dropped. A caller may match, propagate or deliberately transform the failure, without losing the distinction between zero and invalid input.
A changed requirement: stop at the first failure¶
Our current requirement is best-effort reporting. If a configuration loader instead requires all inputs to succeed, put the C++ catch outside the loop and return an error from the overall operation. In Rust, make that operation return Result and propagate with ?. The fixture sequence will attempt three inputs, not four: the first Format stops processing before Range. Its guard count must be three.
That change is not transactional rollback. Earlier successful effects remain unless the application stages changes or defines compensation. To promise an unchanged configuration on failure, parse and validate into temporary owned data first, then commit. Destructor-based cleanup does not undo external writes or report failed commits.
For a C++ non-throwing domain interface, a std::variant
Compile-time rejection is a different kind of failure¶
Save this Rust program separately. It intentionally fails because ? cannot propagate a parse error through this i32 return contract:
Change the return type to Result
A C++ result wrapper likewise is not an integer. Compile this independently; it intentionally fails:
Repair the caller by examining the selected variant and handling both alternatives. A compiler diagnostic is not the runtime error message for a user's malformed input. The successful programs above intentionally accept the program and reject selected input values at runtime.
Panic, invariants and boundary decisions¶
Reserve a panic for an invariant violation or another deliberately non-recoverable path, not normal bad input. Unwrap changes an Err into a panic; it does not make the failure impossible. The catch_unwind documentation warns that it catches unwinding panics, not aborting ones, and is not a replacement for Result-based error handling.
Do not assume C++ exceptions can cross a Rust FFI boundary or that catch_unwind catches arbitrary foreign exceptions. Define an explicit boundary protocol; see unsafe and FFI. Neither this fixture nor destructor tests establish recovery from process termination, aborts, hardware failure or all allocation failures. Keep destructors simple: required fallible shutdown needs a separate operation with a visible result.
| Requirement | Interface choice | Caller responsibility |
|---|---|---|
| Expected invalid input with actionable reasons | Rust Result / C++ domain result or typed exception | Preserve the reason; decide whether to continue |
| Missing value, not an error reason | Option / optional | Distinguish absence from valid zero |
| All-or-nothing application changes | Staging plus explicit commit | Define rollback and external effects separately |
| Broken internal invariant | Assertion/panic or the project's failure policy | Specify whether recovery is meaningful |
| External language boundary | Explicit documented protocol | Translate errors without unintended unwinding |
Exercises and verification¶
- Implement stop-on-first-error in both languages. Assert three cleanup events, and that the fourth input is not processed.
- Repair each compiler-failure example without replacing an error with zero. Check both success and failure alternatives.
- Remove syntax validation and test +1, trailing characters and the long invalid number. Explain why using a built-in parser alone is not the same specification.
- Add input provenance to the domain error while retaining Format/Range classification. Do not expose a borrowed label whose owner expires.
- Explain which effects a guard cleans up and which effects require an explicit transaction protocol. No speed comparison follows from this fixture.
On October 10, 2026, the extracted programs passed on the authorised Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust/Cargo 1.99.0 and GCC g++ 14.2.0. Rust check, exact-source formatting, five debug/release tests and both outputs passed. A stop-first variant retained the tests and produced three cleanup events; the repaired Result caller asserted success and failure. C++20 normal and stop-first outputs passed at -O0/-O2 with assertions enabled, as did the variant repair. The isolated Rust and C++ failure programs were rejected with E0277 and a variant-to-integer conversion diagnostic. Sources and logs were retained; generated Cargo targets were cleaned. These checks do not exercise panic recovery, transactional rollback or performance.
Previous: traits and dispatch · Next: Rust and C++ moves · Course overview