Skip to content

25 - reworked client lib to use Result-Pattern instead of exceptions#69

Merged
tomsontom merged 5 commits into
mainfrom
25-add-support-for-result-pattern-to-java-client-code
Jun 25, 2026
Merged

25 - reworked client lib to use Result-Pattern instead of exceptions#69
tomsontom merged 5 commits into
mainfrom
25-add-support-for-result-pattern-to-java-client-code

Conversation

@tomsontom

Copy link
Copy Markdown
Member

No description provided.

@tomsontom tomsontom linked an issue Jun 24, 2026 that may be closed by this pull request
@tomsontom

Copy link
Copy Markdown
Member Author

Review

Overview

Replaces checked exceptions with a Rust-style Result<T, E> pattern for the generated Java client (java-client-api + java-rest-client-jdk). The server side (java-server) keeps the exception-based model unchanged — error.ts/rsd-exception.ts were correctly split into their own java-server copies so the client-api module's versions could be repurposed for the new pattern.

Key pieces:

  • Result.java (new, java-client-api/result.ts): a sealed Result<T, E> with OK/ERR records, onOk/onErr/or/orThrow.
  • RSDError.java (new, service-errors.ts): a Type enum of all error names, plus one sealed interface per unique combination of errors a given operation can throw (E1, E2, …), each permits $GenericError and the concrete error records that belong to that combination. $GenericError implements every E* interface plus RSDError, so it's always a valid fallback (native exceptions, unexpected HTTP status) regardless of which operation produced it.
  • Each error X = ...; declaration now generates a record X(String message[, T data]) implementing RSDError and every E* combination it participates in — replacing the old XException class hierarchy.
  • Client method signatures now return Result<T, RSDError.E<n>|$GenericError> instead of declaring throws; handleErrorResult/handleOkResult/the generic catch in java-rest-client-jdk/service.ts now return Result.err(...)/return Result.ok(...) instead of throwing.

Verified

  • tsc --noEmit on packages/cli — clean.
  • npm run test --workspace packages/cli — same pre-existing failures as main (network-dependent TS fetch-client tests hitting localhost:3000); no new failures.
  • mvn clean compile / clean test-compile for java-test/java-client (183 main + 12 test sources) — clean.
  • mvn clean compile for java-test/java-quarkus (644 sources, server side) — clean, confirming the server/client error-generation split didn't break the server.
  • Spot-checked that the primitive-vs-boxed return type fix (resolveType(..., true) instead of ..., type.array) in both service.ts files) is required and correct: Result<T, E>'s T is a generic type parameter, so builtin scalar returns (e.g. int) must now always be boxed (Integer) even when not an array — previously the boxing only happened for arrays since the bare method return type could be a primitive.

Potential issue

  • Unused-error edge case produces invalid Java. In java-client-api/error.ts, generateSource builds implements RSDError, ${combinations} where combinations is the joined list of E* interfaces an error participates in (filtered from computeServiceErrorCombination(services)). If an error is declared in the model but never referenced in any operation's throws clause, combinations is '', producing implements RSDError, { — a trailing comma with nothing after it, which won't compile. The bundled sample.rsd happens to reference every declared error, so this doesn't surface in the test fixtures, but it's a real gap for any model that defines an error and doesn't (yet) use it anywhere. Worth either falling back to implements RSDError when there are no combinations, or validating at the language level that every declared error must be referenced by at least one operation.

Minor

  • RSDError.$GenericError and the per-operation generic catch (java-rest-client-jdk/service.ts) construct it positionally as (Type, message, Throwable) — consistent with the record definition, but worth double-checking call sites stay in sync if the record's field order ever changes, since there's no named-argument safety net.

Overall this is a coherent, well-verified rework with good use of Java's sealed interfaces to keep each operation's error type exhaustive-switch-friendly. The one edge case above is the only thing I'd want addressed (or explicitly accepted as a known limitation) before merge.

@tomsontom

Copy link
Copy Markdown
Member Author

Follow-up: the unused-error edge case I flagged is fixed in 7d56ffe. `error.ts` now builds the `implements` list as an array seeded with `'RSDError'` via `combinations.unshift('RSDError')` before joining, so it always renders `implements RSDError` (plus any `E*` combinations) instead of producing a dangling trailing comma when an error isn't referenced by any operation. Re-verified `tsc --noEmit` and a clean `mvn compile` on `java-test/java-client` — both pass. No remaining concerns from my side.

@tomsontom

Copy link
Copy Markdown
Member Author

Re-checked after 1ad1edd ("make rsderror and sealed interface"): making `RSDError` itself a sealed interface with an explicit `permits` list is a nice tightening — confirmed it compiles and the permits set lines up with the set of direct implementors (every error record always `implements RSDError` per the earlier fix, plus `$GenericError`).

However, the same class of bug as before still exists, just relocated — in `service-errors.ts`, the `$GenericError` record is generated as:

```ts
public record $GenericError(Type type, String message, Throwable error) implements RSDError, ${ errorNames.length > 0 ? errorNames.map(({ interfaceName }) => interfaceName).join(', ') : '' } {
```

If `errorNames` (the computed service-error combinations) is empty — i.e. a model with no `error` declarations at all, or one where no operation has a `throws` clause — this renders as `implements RSDError, {` (trailing comma, nothing after it), which won't compile. I verified this by evaluating the same ternary standalone with an empty array. None of the bundled `.rsd` test specs hit this (they all declare errors and throws), so it won't surface in CI as-is, but it's the identical root cause as the issue fixed in 7d56ffe, just not yet applied here. Same fix shape would work: build the implements list as an array seeded with `'RSDError'` and conditionally push the combination names, then `.join(', ')`.

@tomsontom

Copy link
Copy Markdown
Member Author

Re-checked after 8efca69: the `$GenericError` trailing-comma issue is fixed correctly — `implements RSDError${errorNames.length > 0 ? ', ' + ... : ''}` now renders `implements RSDError` alone when there are no combinations, or `implements RSDError, E1, E2` when there are, with no dangling comma either way. Verified `tsc --noEmit` and `mvn clean compile` on `java-test/java-client` both pass. No remaining concerns — LGTM.

@tomsontom tomsontom merged commit 7ec2a05 into main Jun 25, 2026
2 checks passed
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.

Add support for Result-Pattern to Java-Client code

1 participant