Skip to content

Fix Result type generation for user-defined error enums named "Error" - #1709

Closed
willemneal wants to merge 2 commits into
stellar:mainfrom
theahaco:fix/error_generation
Closed

Fix Result type generation for user-defined error enums named "Error"#1709
willemneal wants to merge 2 commits into
stellar:mainfrom
theahaco:fix/error_generation

Conversation

@willemneal

@willemneal willemneal commented Jan 29, 2026

Copy link
Copy Markdown
Contributor

What

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 lives entirely in client generation. The on-the-wire spec format is unchanged: a contract function returning Result<_, Error> still serializes the error type as 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 to Udt { 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::Error continues to map to soroban_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 returned Result<_, soroban_sdk::Error> instead of Result<_, 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:

  1. No spec wire-format change. The bytes that go into a deployed wasm are identical to what main produces today, 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 — no redeploy required.

Example — before the fix:

// Contract defines:
pub enum Error { Overflow = 1 }
pub fn safe_add(a: u64, b: u64) -> Result<u64, Error>

// Generated client had:
fn try_safe_add(...) -> Result<..., Result<soroban_sdk::Error, ...>>

Example — after the fix:

// Generated client now correctly has:
fn try_safe_add(...) -> Result<..., Result<Error, ...>>

Closes #1710

Known limitations

If a contract both defines an error enum named Error and uses soroban_sdk::Error directly at a function boundary, every Error reference 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 enum Error will not also be using the SDK error type by that name.

Co-Authored-By: Claude Opus 4.5 noreply@anthropic.com
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

Copilot AI review requested due to automatic review settings January 29, 2026 18:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an issue where user-defined error enums named "Error" were incorrectly mapped to the built-in soroban_sdk::Error type instead of being treated as user-defined types (UDTs). The fix ensures that all error enums in contract return types are properly represented as UDTs in the contract specification.

Changes:

  • Removed special-case handling of "Error" as a built-in type in type mapping
  • Added comprehensive test coverage for Result types with user-defined error enums
  • Verified the fix works for both "Error" and "MyError" custom error types

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
soroban-sdk-macros/src/map_type.rs Removed line that mapped "Error" to built-in error type; added unit tests verifying "Error" and "MyError" map to UDTs
tests/add_u64/src/lib.rs Added two error enums and safe_add functions to test Result type generation
tests/import_contract/src/lib.rs Added Error enum and functions that use imported contract's error types
tests/add_u64/test_snapshots/test/test_safe_add.1.json Added ledger snapshot for new test case
soroban-spec-rust/src/lib.rs Added integration tests verifying correct Result type generation for user-defined errors

@willemneal

Copy link
Copy Markdown
Contributor Author

I used this as a test for Claude at finding and fixing the bug I found. I provided the minimal example and it found and fixed it very quickly. And now it being reviewed by another AI which had no comments feels like a new era.

@leighmcculloch My only question is whether there would ever be the case that the Error type in ScTypeDef would be found in a contract spec. Is it supposed to map to soroban_sdk::Error?

@leighmcculloch

Copy link
Copy Markdown
Member

Can you open an issue to go into detail about the problem to make sure we fully capture this bug, it's scope of impact, before we focus on the solution?

We can do some analysis of contracts on mainnet to understand how changing the existing behaviour might break existing contracts, or if there's any impact today that we might need to think differently about.

I suspect this fix is right though when we come back to it.

Side note, this might be resolved by planned work around improving type identities (#1570).

@leighmcculloch

Copy link
Copy Markdown
Member

My only question is whether there would ever be the case that the Error type in ScTypeDef would be found in a contract spec. Is it supposed to map to soroban_sdk::Error?

Yes I think so. Error is an SDK type, it's a valid type to appear at a contract boundary. Whether it is frequently used is a good question though, it may not be.

@leighmcculloch

Copy link
Copy Markdown
Member

Although the Error type captures a much larger range of errors than what a contract fn could return, so there's an argument for not supporting it, or finding some other way to capture that use case. 🤔

@leighmcculloch

Copy link
Copy Markdown
Member

@willemneal
willemneal force-pushed the fix/error_generation branch from b5bc732 to db61291 Compare April 15, 2026 19:25

@leighmcculloch leighmcculloch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment inline. I'd like to flip the solution, same outcome, but more compatible with all existing contracts. Is there a reason you're leaning towards changing how contract specs are built, rather than changing how clients interpret them?

Comment thread soroban-spec-rust/src/lib.rs Outdated
Comment on lines +155 to +158
/// The `#[contractimpl]` macro emits any type named `Error` as a UDT reference
/// in the spec, so a contract that uses `soroban_sdk::Error` directly (rather
/// than defining its own error enum) produces a spec with
/// `Udt { name: "Error" }` but no matching error-enum entry. When that

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should do the reverse, emit a non-UDT Error.

Why: to reduce the observable behaviour change in contracts themselves, and shift the change to be in the interpretation of the specs.

So any type named Error always gets stored in the spec as the SDK Error type, which is what happens today.

Then, when a client sees the Error type, they first check for a UDT also named Error and use that instead. This I think shifts all the behaviour change to the CLI, which also means all existing contracts can benefit from this too.

willemneal added a commit to theahaco/rs-soroban-sdk that referenced this pull request Apr 27, 2026
Restore the `"Error" => ScSpecTypeDef::Error` arm so the spec wire-format
remains unchanged from prior releases. The user-defined-vs-SDK Error
disambiguation moves to client code generation in the next commit, which
also lets already-deployed contracts benefit without needing to be
rebuilt.

Per review feedback on stellar#1709:
stellar#1709 (comment)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
willemneal added a commit to theahaco/rs-soroban-sdk that referenced this pull request Apr 27, 2026
Replace the spec-side `apply_error_udt_fallback` (which rewrote
`Udt { name: "Error" }` → `ScSpecTypeDef::Error` when no error enum
named `Error` was defined) with the inverse `apply_error_udt_override`
(which rewrites `ScSpecTypeDef::Error` → `Udt { name: "Error" }` when
the spec *does* define an error enum named `Error`).

This keeps the on-the-wire spec format identical to what every release
has produced: a contract function returning `Result<_, Error>` still
serializes its error type as the built-in `ScSpecTypeDef::Error`. The
override fires only at client generation, so already-deployed contracts
that define an `Error` error enum get a generated client referencing
that UDT instead of `soroban_sdk::Error` — without any redeploy.

Tests updated accordingly:
- `test_add_u64_spec_entries` now guards that the wasm spec still emits
  `ScSpecTypeDef::Error` (and `Udt { name: "MyError" }` for the
  differently-named enum).
- `test_missing_error_udt_falls_back_to_sdk_error` and
  `test_error_udt_overrides_sdk_error` (renamed) build hand-crafted
  specs whose function returns `Result<_, ScSpecTypeDef::Error>`,
  asserting the override only fires when a matching UDT is present.

Per review feedback on stellar#1709:
stellar#1709 (comment)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@willemneal
willemneal force-pushed the fix/error_generation branch from 5d01fac to 960b1a9 Compare April 27, 2026 14:50
willemneal added a commit to theahaco/rs-soroban-sdk that referenced this pull request Apr 27, 2026
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 stellar#1709:
stellar#1709 (comment)

Closes stellar#1710

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 stellar#1709:
stellar#1709 (comment)

Closes stellar#1710

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@willemneal
willemneal force-pushed the fix/error_generation branch from 75e7dea to 221be18 Compare April 27, 2026 15:54
@willemneal

Copy link
Copy Markdown
Contributor Author

@leighmcculloch Great point! I updated with an inversion. And updated the generated artifacts since there are now the error types.

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>
@willemneal
willemneal force-pushed the fix/error_generation branch from 221be18 to 6350260 Compare April 27, 2026 19:25
@leighmcculloch

Copy link
Copy Markdown
Member

@willemneal could you resolve conflicts? Thanks!

@mootz12

mootz12 commented May 6, 2026

Copy link
Copy Markdown
Contributor

Closed in favor of #1862

@mootz12 mootz12 closed this May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generated client code uses wrong Error type when user-defined error enum is named "Error"

4 participants