Skip to content

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

Closed
moetemp wants to merge 1 commit into
review/p3-closefrom
review/p3-errors
Closed

moetemp wants to merge 1 commit into
review/p3-closefrom
review/p3-errors

Conversation

@moetemp

@moetemp moetemp commented Oct 28, 2026

Copy link
Copy Markdown
Owner

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 temporalio/nexgen#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 is for review here before it goes upstream. It sits on the upstream stack temporalio#308, temporalio#310, temporalio#311, temporalio#297, temporalio#296, temporalio#303, temporalio#304, temporalio#309, temporalio#318, temporalio#319, temporalio#320, temporalio#321, temporalio#322, temporalio#323, temporalio#324, temporalio#325, whose head is review/p3-base, and on #77, #79, #81, #83, #86, #88, #90, #91, #93.

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
Owner Author

Duplicate of #95, which has the same commit.

@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