Skip to content

feat: map the cases of an error enum to exception classes - #335

Closed
moetemp wants to merge 1 commit into
moe/up/resource-closefrom
moe/up/error-enums
Closed

moetemp wants to merge 1 commit into
moe/up/resource-closefrom
moe/up/error-enums

Conversation

@moetemp

@moetemp moetemp commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

What changed?

This PR adds @nexus.error, which maps the cases of an error enum to exception classes.

/// @nexus.proto "temporal.api.enums.v1.StreamErrorReason"
/// @nexus.error details="stream-error-details" field="reason" base="stream-error"
///   python-base="temporalio.exceptions.ApplicationError"
///   typescript-base="ApplicationFailure" typescript-base-import="@temporalio/common"
///   dotnet-base="Temporalio.Exceptions.ApplicationFailureException"
enum stream-error-reason {
  stream-error-reason-unspecified,
  /// @nexus.error
  stream-error-reason-not-found,
  /// @nexus.error retryable=true
  stream-error-reason-storage,
  stream-error-reason-sequence,
}
  • On an enum, @nexus.error makes a class per case that has the directive, under one base. details and field name the record and its field that carry the case in a failed call. <language>-base names the SDK class that the base extends, and a language with none gets no classes.
  • Names. The base is StreamError in Python and TypeScript, and StreamException in .NET. A case class is the stem, the case without the enum's prefix, and the suffix, such as StreamSeqDivergentError. name and <language>-name on a case override it.
  • A case without the directive, and a case that this code doesn't know, raise the base class, which keeps the reason's number. This relies on the open enums of feat: decode an unknown proto enum number as a marked fallback case #321.
  • Each class is non-retryable, except a case with retryable=true. Its failure type is the class name, and its details hold the details record, so it survives a Local Activity.
  • Python gives errors.py with stream_error(details, message), TypeScript gives errors.ts with streamError(details, message), and .NET gives Errors.cs with StreamErrors.From(details, message).
  • In Python, each operations object of the interface converts a failure. That is the client extension's private operations and the Workflow operations class. It reads the details with _support.error_details(err, StreamErrorDetails), and raises the mapped class from the original error. Other errors propagate.
  • The error enum and its details record are now generated, though no operation names them. An error enum of a used package stays out, so the writer package doesn't repeat them.
  • Go and Java ignore the directive with a warning.

nexgen refuses an enum @nexus.error with no details, field or base, a details that isn't a record of the interface, a field that doesn't hold the enum, a case @nexus.error with none on the enum, two cases with one class name, a retryable that isn't true or false, and two error enums in one interface.

The stream sample takes the error directives of Brook's next WIT. The sample support gives error_details, which finds the details record in the cause chain of a Nexus operation failure or of an Activity error, as the model or as the proto message.

It sits on #334. The stack is #308, #310, #311, #297, #296, #303, #304, #309, #318, #319, #320, #321, #322, #323, #324, #325, #326, #327, #328, #329, #330, #331, #332, #333, #334, then this PR.

Why?

The Stream API of the proposal raises one error class per Store reason, such as StreamNotFoundError. The facade maps the reasons by hand today, in _errors.py.

TypeScript and .NET get the classes and the mapping function, but their operations don't convert failures yet. The conversion sits in the operations objects, and those come with their client targets. The design passed int(reason) as the detail. This PR passes the details record, so the support helper finds it as the same type on both sides.

How did you test it?

Python tests check that a reason gives its class and retryability, that a reason with no class and an unknown reason give the base class, and that the details come from a Nexus operation failure and from a Local Activity's error. Others check that a client call raises the class of its reason from the original error, that a call with no reason raises its own error, and that a Workflow resource method raises the class of a Local Activity failure. TypeScript and .NET tests check that a reason gives its class and retryability, and the base class for the others. A Go test checks the warning, a unit test checks the directive, and rejection fixtures cover each refusal.
cargo validate passes.

`@nexus.error` on an enum makes a class per case of a failed call, under one base. Python's
operations objects raise the class of a failed call's reason from the original error.
@moetemp

moetemp commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Moved to moetemp/nexgen#95 for review first. It comes back here after that.

@moetemp moetemp closed this Oct 28, 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.

1 participant