Fix Result type generation for user-defined error enums named "Error"#1862
Conversation
When a contract defines its own error enum named `Error` and uses it
in a `Result<T, Error>` return type, the generated client code now
emits `Result<T, Error>` (referencing the user-defined enum) instead
of `Result<T, soroban_sdk::Error>`.
The fix is contained to client generation. Spec emission is unchanged:
a contract function returning `Result<_, Error>` still serializes the
error type as the built-in `ScSpecTypeDef::Error` exactly as every
prior SDK release has done. During Rust client generation,
`soroban-spec-rust` now checks whether the spec also contains a
`UdtErrorEnumV0` named `Error`, and if so rewrites every
`ScSpecTypeDef::Error` reference in the spec to `Udt { name: "Error" }`
so that the generated trait references the user-defined type.
If no error enum named `Error` is present, behavior is unchanged:
`ScSpecTypeDef::Error` continues to map to `soroban_sdk::Error`,
covering contracts that intentionally use the SDK error type at
their boundary.
Two benefits over the originally-proposed approach of changing what
`#[contractimpl]` emits:
1. No spec wire-format change — bytes that go into a deployed wasm
are identical to what `main` produces, so no contract's spec hash
drifts.
2. Already-deployed contracts benefit immediately — any mainnet
contract that defines an `Error` error enum will, the next time
someone regenerates a Rust client against its spec with the new
`soroban-spec-rust`, produce a correct `Result<_, Error>` signature
without any redeploy.
Per review feedback on #1709:
#1709 (comment)
Closes #1710
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The test_add_u64 and test_import_contract crates now define `safe_add`/`safe_add_two` (and `safe_add_with`/`safe_add_with_two` on the importer side) returning `Result<u64, Error>` / `Result<u64, MyError>`, so their `tests-expanded/*.rs` snapshots — checked by the `expand-test-wasms` CI job — needed regeneration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Claude Code Review
|
|
Claude finished @mootz12's task in 2m 57s —— View job Claude Code ReviewApproach: focused review on
SummaryThe fix is well-scoped and well-documented. The chosen approach — rewriting Generated code in Below are observations ordered by significance. None are blocking. Observations1.
|
There was a problem hiding this comment.
Pull request overview
This PR fixes Rust client generation for contracts that define a user error enum named Error and use it as the Err side of a Result<T, Error>: generated clients now reference the user-defined Error UDT instead of soroban_sdk::Error. The fix is implemented in soroban-spec-rust by rewriting ScSpecTypeDef::Error to Udt { name: "Error" } during client generation when the spec includes a UdtErrorEnumV0 named Error, preserving the on-the-wire spec format while improving client type fidelity (including for already-deployed contracts).
Changes:
- Add a spec pre-pass in
soroban-spec-rustthat conditionally rewritesScSpecTypeDef::Errorreferences to theErrorUDT when anErrorerror-enum UDT is present. - Add/extend test contracts and snapshots to cover both
Errorand non-Erroruser-defined error enums (MyError) across direct and imported-client usage. - Add generator-level unit tests validating both (a) the generated Rust signatures and (b) that the wasm spec itself still contains
ScSpecTypeDef::ErrorforError.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
soroban-spec-rust/src/lib.rs |
Implements the conditional spec rewrite pass and adds unit tests to validate correct Rust output and unchanged spec wire format. |
tests/add_u64/src/lib.rs |
Adds safe_add/safe_add_two entrypoints and defines Error/MyError error enums to exercise the generation behavior. |
tests/add_u64/test_snapshots/test/test_safe_add.1.json |
Adds snapshot output for the new safe_add test coverage. |
tests/import_contract/src/lib.rs |
Adds an importing contract that calls into the add_u64 contract and validates error mapping through generated clients. |
tests-expanded/test_add_u64_wasm32v1-none.rs |
Updates committed expanded output reflecting the new error enums and Result signatures in the add_u64 test contract. |
tests-expanded/test_add_u64_tests.rs |
Updates committed expanded output reflecting the new add_u64 tests and generated client signatures. |
tests-expanded/test_import_contract_wasm32v1-none.rs |
Updates committed expanded output reflecting the new imported-client calls and error enum in the import_contract test. |
tests-expanded/test_import_contract_tests.rs |
Updates committed expanded output reflecting the new import_contract tests and generated client signatures. |
1.
|
|
Claude finished @mootz12's task in 1m 33s —— View job Claude Code Review (follow-up)
ResponsesPoint #1 (
|
|
Claude finished @mootz12's task in 49s —— View job Claude Code Review (sync follow-up)
The new commit
VerdictLGTM — snapshot-only regeneration commit, consistent with the source change it follows. No new review concerns. PR remains in the approved-from-my-end state from the previous round. |

What
When a contract defines its own error enum named
Errorand uses it in aResult<T, Error>return type, the generated client code now emitsResult<T, Error>(referencing the user-defined enum) instead ofResult<T, soroban_sdk::Error>.The fix lives entirely in client generation. The on-the-wire spec format is unchanged: a contract function returning
Result<_, Error>still serializes the error type asScSpecTypeDef::Error, exactly as every prior SDK release has done. During Rust client generation,soroban-spec-rustnow checks whether the spec also contains aUdtErrorEnumV0namedError, and if so rewrites everyScSpecTypeDef::Errorreference toUdt { name: "Error" }so that the generated trait references the user-defined type.If the spec contains no error enum named
Error, behavior is unchanged:ScSpecTypeDef::Errorcontinues to map tosoroban_sdk::Error, which covers contracts that intentionally use the SDK error type at their boundary.Why
Previously, contracts whose error enum was named
Error(a natural choice — it's what every other Rust crate would name it) silently produced a generated client that returnedResult<_, soroban_sdk::Error>instead ofResult<_, Error>, hiding the contract's error variants behind the opaque SDK error type and breaking client-side error matching.This iteration of the fix moves the resolution from spec emission to client interpretation, per review feedback (#1709 (comment)). Two benefits over the original approach of changing what
#[contractimpl]emits:mainproduces today, so no contract's spec hash drifts.Errorerror enum will, the next time someone regenerates a Rust client against its spec with the newsoroban-spec-rust, produce a correctResult<_, Error>signature — no redeploy required.Example — before the fix:
Example — after the fix:
Closes #1710
Known limitations
If a contract both defines an error enum named
Errorand usessoroban_sdk::Errordirectly at a function boundary, everyErrorreference in the spec resolves to the user-defined UDT — there is no way to mix the two in one spec. This matches user intent: a contract that names its own error enumErrorwill not also be using the SDK error type by that name.Credit
This PR was forked from #1709 from @willemneal to allow maintainers to handle merge conflicts.