Skip to content

feat!: Standardize Error Handling and Support Custom Error Reasons - #196

Open
natemort wants to merge 1 commit into
cadence-workflow:mainfrom
natemort:errors_v2
Open

natemort wants to merge 1 commit into
cadence-workflow:mainfrom
natemort:errors_v2

Conversation

@natemort

@natemort natemort commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

This is a breaking change!

Standardize the exception hierarchy, introducing CadenceError as the root of all Errors and FailureError as the root of any Failure expected in Workflow code. We call it FailureError rather than WorkflowError as ApplicationError is raised by Activity implementations.

Add ApplicationError. This allows users to explicitly specify the error reason, details, and any FailureOptions from a Workflow or an Activity. If a Workflow or Activity raises any other type of Error then the reason will be the name of the error type and the details will be a serialized representation of the error. We don't attempt to make this representation of the error round-trippable. It's purely for manual inspection and debugging, not to propagate stacktraces through a Workflow.

Within Workflows we introduce a consistent hierarchy for receiving exceptions from Activities or Child Workflows.

For Activities:

  • If cancelled, we raise asyncio.CancelledError
  • Else, we raise ActivityError. The reason for the error is the backing __cause__, which we also expose as a property:
    • ApplicationError if the activity failed. This contains the reason and details.
    • CadenceTimeoutError if the activity timed out.

For Child Workflows:

  • If cancelled by the Workflow, we raise asyncio.CancelledError
  • If we failed to start the Child Workflow, we raise StartChildWorkflowExecutionError (extends ChildWorkflowError)
  • Else, we raise ChildWorkflowError. Again the reason is the backing cause:
    • ApplicationError if the child workflow failed.
    • WorkflowCanceledError if the child workflow was cancelled externally
    • CadenceTimeoutError if the child workflow timed out.
    • TerminatedError if the child worfklow was terminated.

Distinguishing between cancellation from within the Workflow and external Cancellation is important because asyncio.CancelledError extends BaseException instead of Exception. It's unlikely that Workflow code expects a non-Exception type if they're not the one cancelling the Workflow. This aligns with the Java SDK.

SignalFailure is removed, it was an implementation detail that was never exposed to users. A failing signal handler now fails the decision task via fail_decision with a FatalDecisionError whose cause is the handler's exception, consistent with other fatal decision errors.

The breaking changes are:

  • Removed most subtypes of ChildWorkflowError to align it with Activities.
  • Renamed all errors to end with Error:
    • ActivityFailure -> ActivityError
    • WorkflowFailure -> FailureError
    • SignalExternalWorkflowFailed -> SignalExternalWorkflowError
    • StartChildWorkflowExecutionFailed -> StartChildWorkflowExecutionError
  • Renamed StartChildWorkflowExecutionFailed.cause -> failure_cause to avoid confusion with Error cause.

What changed?

  • Added consistent behavior around error types and handling in Workflows.
  • Added ApplicationError to support an explicit failure reason

Why?

  • Make error handling clearer and consistent.
  • Support arbitrary error reasons, similar to Go SDK.

How did you test it?

  • Unit and integration tests.

Potential risks

  • This is a breaking change.

Release notes

Documentation Changes

Comment thread cadence/_internal/failure.py Resolved
Comment thread cadence/error.py Resolved
Comment thread cadence/error.py Resolved
Comment on lines +156 to +157
if cancel_requested:
self.result.force_cancel(event.details.data.decode())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Edge Case: Strict UTF-8 decode of child cancel details can crash the decision task

When the parent has requested cancellation, handle_canceled calls self.result.force_cancel(event.details.data.decode()). This decodes the raw details payload as strict UTF-8. That payload comes from the pluggable DataConverter (or from whichever client canceled the child), so it is not guaranteed to be valid UTF-8; a binary converter such as protobuf or msgpack will produce invalid bytes. When it is not, a UnicodeDecodeError is raised inside the history event handler, which fails the decision task, and replaying the same history fails again every time. The rest of this PR decodes untrusted payload bytes with errors="replace" (see ApplicationError._from_payload). Do the same here, or leave the details out of the cancel message entirely.

Decode leniently so malformed bytes can't raise in the event handler:

self.result.force_cancel(
    event.details.data.decode("utf-8", errors="replace")
)
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

Comment thread cadence/error.py
Comment on lines +246 to +260
class ActivityError(FailureError):
"""An activity failed or timed out.

``cause`` is an :class:`ApplicationError` if the activity failed, or a
:class:`CadenceTimeoutError` if it timed out. Cancellation is raised as
:class:`asyncio.CancelledError` instead.
"""

class SignalExternalWorkflowFailed(Exception):
def __init__(self, message: str, cause: Any) -> None:
def __init__(self, message: str, *, activity_id: str, activity_type: str) -> None:
super().__init__(message)
self.cause = cause
self.activity_id = activity_id
self.activity_type = activity_type

@property
def cause(self) -> BaseException | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Cross-Repo: Cadence-Docs Python error guide still uses removed error classes

Fix here and in cadence-workflow/Cadence-Docs

Breaks in: cadence-workflow/Cadence-Docs - docs/07-python-client/09-error-handling.md - Python error handling guide (Activity failures, Child workflow failures, Signal failures sections and Error reference table)

The Python error-handling guide in cadence-workflow/Cadence-Docs (docs/07-python-client/09-error-handling.md) imports and catches ActivityFailure, StartChildWorkflowExecutionFailed, ChildWorkflowExecution{Failed,Canceled,TimedOut,Terminated}, SignalExternalWorkflowFailed, WorkflowFailure and SignalFailure from cadence.error. This PR removes or renames all of these to ActivityError, ChildWorkflowError (with a cause), StartChildWorkflowExecutionError, SignalExternalWorkflowError and FailureError, and cadence/error.py defines no aliases for the old names. So every snippet in the guide now raises ImportError. The guide's statement that the five child-workflow classes are subclasses of ChildWorkflowError is also no longer true: a failure now arrives as a ChildWorkflowError whose cause is an ApplicationError, CadenceTimeoutError, TerminatedError or WorkflowCanceledError, and cancellation the parent requested surfaces as asyncio.CancelledError. The break is intentional, but the docs need a coordinated update. The PR description also promises aliases that the code does not provide.

Here: Add the deprecated aliases the PR description promises so the old imports keep resolving during migration, and link the docs update from the release notes.

There: Rewrite the Activity, Child workflow and Signal failure sections and the Error reference table in docs/07-python-client/09-error-handling.md for the new hierarchy: catch ActivityError / ChildWorkflowError and inspect .cause, use StartChildWorkflowExecutionError.failure_cause and SignalExternalWorkflowError, drop SignalFailure, and document ApplicationError and asyncio.CancelledError for cancellation the workflow itself requested.

In cadence-workflow/Cadence-Docs - docs/07-python-client/09-error-handling.md:

from cadence.error import ActivityError, ApplicationError, CadenceTimeoutError

try:
    await workflow.execute_activity(...)
except ActivityError as e:
    if isinstance(e.cause, ApplicationError):
        print(e.cause.reason, e.cause.details())
    elif isinstance(e.cause, CadenceTimeoutError):
        print(e.cause.timeout_type)

Hold the contract here:

# Deprecated aliases kept for backwards compatibility
ActivityFailure = ActivityError
WorkflowFailure = FailureError
SignalExternalWorkflowFailed = SignalExternalWorkflowError
StartChildWorkflowExecutionFailed = StartChildWorkflowExecutionError
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.38462% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cadence/error.py 94.87% 3 Missing and 3 partials ⚠️
cadence/_internal/failure.py 97.82% 2 Missing ⚠️
...emachine/child_workflow_execution_state_machine.py 84.61% 1 Missing and 1 partial ⚠️
cadence/_internal/workflow/workflow_engine.py 80.00% 1 Missing ⚠️
cadence/testing/_workflow_environment.py 91.66% 1 Missing ⚠️
Files with missing lines Coverage Δ
cadence/_internal/activity/_activity_executor.py 94.82% <100.00%> (+0.04%) ⬆️
...al/workflow/statemachine/activity_state_machine.py 95.45% <100.00%> (-0.07%) ⬇️
...internal/workflow/statemachine/decision_manager.py 94.98% <100.00%> (+0.03%) ⬆️
...emachine/signal_external_workflow_state_machine.py 100.00% <100.00%> (ø)
cadence/_internal/workflow/workflow_instance.py 85.71% <100.00%> (-0.50%) ⬇️
cadence/_internal/workflow/workflow_engine.py 90.78% <80.00%> (-0.38%) ⬇️
cadence/testing/_workflow_environment.py 87.35% <91.66%> (+0.86%) ⬆️
cadence/_internal/failure.py 97.82% <97.82%> (ø)
...emachine/child_workflow_execution_state_machine.py 95.04% <84.61%> (-1.76%) ⬇️
cadence/error.py 95.58% <94.87%> (+0.38%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines 139 to +153
self._resolve(
self.result,
exc=ChildWorkflowExecutionFailed(
event.failure.reason,
failure=event.failure,
),
exc=child_workflow_failed_error(event, self._data_converter),
)

@child_workflow_events.event()
def handle_canceled(
self, event: history.ChildWorkflowExecutionCanceledEventAttributes
) -> None:
self._transition(DecisionState.COMPLETED)
self._resolve(
self.result,
exc=ChildWorkflowExecutionCanceled(
"child workflow canceled", details=event.details
),
cancel_requested = self.state in (
DecisionState.CANCELED_AFTER_RECORDED,
DecisionState.CANCELED_AFTER_STARTED,
DecisionState.CANCELLATION_RECORDED,
)
self._transition(DecisionState.COMPLETED)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ [cross-pr] Merging #199 drops data_converter and uses removed error names

Breaks when this PR and #199 both merge, and the one that lands second keeps #199's version of the state-machine construction and failure handlers. The breakage shows up as soon as an activity or child workflow fails, or the module is imported.

How

  1. This PR requires data_converter in ActivityStateMachine(request, completed, data_converter) (activity_state_machine.py:24-29). DecisionManager.__init__ gets it as an extra argument (decision_manager.py:129-135), and WorkflowEngine passes info.data_converter (workflow_engine.py:65).
  2. feat!: Return Futures Instead of Coroutines #199 rewrites DecisionManager.__init__(event_loop) and the schedule_activity / schedule_child_workflow call sites. They build ActivityStateMachine(attrs, future) and ChildWorkflowExecutionStateMachine(attrs, execution, result) with no data_converter.
  3. feat!: Return Futures Instead of Coroutines #199's handle_failed / handle_canceled / handle_timeout / handle_initiation_failed still raise ActivityFailure, ChildWorkflowExecutionCanceled and the other old names. cadence/error.py in this PR no longer defines any of them, and it adds no aliases either (only ActivityError, ChildWorkflowError, StartChildWorkflowExecutionError, etc. at error.py:246-320). Keeping feat!: Return Futures Instead of Coroutines #199's lines therefore fails at import, or with a TypeError on construction. Keeping only part of them means ApplicationError details are never decoded and the parent-requested-cancel force_cancel path is lost.

Fix

  • This PR: nothing needed beyond keeping the DecisionFuture/ChildWorkflowFutureImpl handling for result if this PR lands second.
  • feat!: Return Futures Instead of Coroutines #199: thread data_converter through DecisionManager into both state machines, and build errors with the cadence/_internal/failure.py factories (activity_failed_error, child_workflow_*_error) instead of the removed exception names. Also update its state-machine tests to the new constructor signatures.

Compared 2be6a55 with #199 at c301d52

Was this helpful? React with 👍 / 👎

This is a breaking change!

Standardize the exception hierarchy, introducing CadenceError as the root of all Errors and FailureError as the root of any Failure expected in Workflow code. We call it FailureError rather than WorkflowError as ApplicationError is raised by Activity implementations.

Add ApplicationError. This allows users to explicitly specify the error reason, details, and any FailureOptions from a Workflow or an Activity. If a Workflow or Activity raises any other type of Error then the reason will be the name of the error type and the details will be a serialized representation of the error. We don't attempt to make this representation of the error round-trippable. It's purely for manual inspection and debugging, not to propagate stacktraces through a Workflow.

Within Workflows we introduce a consistent hierarchy for receiving exceptions from Activities or Child Workflows.

For Activities:
- If cancelled, we raise asyncio.CancelledError
- Else, we raise ActivityError. The reason for the error is the backing `__cause__`, which we also expose as a property:
  - ApplicationError if the activity failed. This contains the reason and details.
  - CadenceTimeoutError if the activity timed out.

For Child Workflows:
- If cancelled by the Workflow, we raise asyncio.CancelledError
- If we failed to start the Child Workflow, we raise StartChildWorkflowExecutionError (extends ChildWorkflowError)
- Else, we raise ChildWorkflowError. Again the reason is the backing cause:
  - ApplicationError if the child workflow failed.
  - WorkflowCanceledError if the child workflow was cancelled externally
  - CadenceTimeoutError if the child workflow timed out.
  - TerminatedError if the child worfklow was terminated.

Distinguishing between cancellation from within the Workflow and external Cancellation is important because asyncio.CancelledError extends BaseException instead of Exception. It's unlikely that Workflow code expects a non-Exception type if they're not the one cancelling the Workflow. This aligns with the Java SDK.

SignalFailure is removed, it was an implementation detail that was never exposed to users. A failing signal handler now fails the decision task via fail_decision with a FatalDecisionError whose __cause__ is the handler's exception, consistent with other fatal decision errors.

The breaking changes are:
- Removed most subtypes of ChildWorkflowError to align it with Activities.
- Renamed all errors to end with Error:
  - ActivityFailure -> ActivityError
  - WorkflowFailure -> FailureError
  - SignalExternalWorkflowFailed -> SignalExternalWorkflowError
  - StartChildWorkflowExecutionFailed -> StartChildWorkflowExecutionError
- Renamed StartChildWorkflowExecutionFailed.cause -> failure_cause to avoid confusion with Error `cause`.
- TestWorkflowEnvironment.get_workflow_error() returns a FatalDecisionError (caused by the handler's exception) when a signal handler fails.
@gitar-bot

gitar-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 3 closed / 6 findings

🔴 High risk · Renaming and removing public exception types while changing failure serialization could break existing workflow handlers and alter retry behavior after an upgrade.

Standardizes the Python client exception hierarchy under CadenceError, adds ApplicationError for explicit failure reasons and details, and adds cadence/_internal/failure.py for converting exceptions to and from Failure messages. Changes requested before merge: strict UTF-8 decoding of child cancel details in child_workflow_execution_state_machine.py can fail the decision task on non-UTF-8 payloads, and the Cadence-Docs Python error-handling guide still references removed error classes such as ActivityFailure and SignalFailure, which will raise ImportError. The deprecated aliases promised in the PR description are also missing from cadence/error.py. Merging with #199 will also break unless data_converter is threaded through DecisionManager and #199 builds errors with the new failure.py factories.

⚠️ Cross-Repo: Cadence-Docs Python error guide still uses removed error classes

📄 cadence/error.py:246-260 📄 cadence/error.py:57-62

Fix here and in cadence-workflow/Cadence-Docs

Breaks in: cadence-workflow/Cadence-Docs - docs/07-python-client/09-error-handling.md - Python error handling guide (Activity failures, Child workflow failures, Signal failures sections and Error reference table)

The Python error-handling guide in cadence-workflow/Cadence-Docs (docs/07-python-client/09-error-handling.md) imports and catches ActivityFailure, StartChildWorkflowExecutionFailed, ChildWorkflowExecution{Failed,Canceled,TimedOut,Terminated}, SignalExternalWorkflowFailed, WorkflowFailure and SignalFailure from cadence.error. This PR removes or renames all of these to ActivityError, ChildWorkflowError (with a cause), StartChildWorkflowExecutionError, SignalExternalWorkflowError and FailureError, and cadence/error.py defines no aliases for the old names. So every snippet in the guide now raises ImportError. The guide's statement that the five child-workflow classes are subclasses of ChildWorkflowError is also no longer true: a failure now arrives as a ChildWorkflowError whose cause is an ApplicationError, CadenceTimeoutError, TerminatedError or WorkflowCanceledError, and cancellation the parent requested surfaces as asyncio.CancelledError. The break is intentional, but the docs need a coordinated update. The PR description also promises aliases that the code does not provide.

Here: Add the deprecated aliases the PR description promises so the old imports keep resolving during migration, and link the docs update from the release notes.

There: Rewrite the Activity, Child workflow and Signal failure sections and the Error reference table in docs/07-python-client/09-error-handling.md for the new hierarchy: catch ActivityError / ChildWorkflowError and inspect .cause, use StartChildWorkflowExecutionError.failure_cause and SignalExternalWorkflowError, drop SignalFailure, and document ApplicationError and asyncio.CancelledError for cancellation the workflow itself requested.

In cadence-workflow/Cadence-Docs - docs/07-python-client/09-error-handling.md:

from cadence.error import ActivityError, ApplicationError, CadenceTimeoutError

try:
    await workflow.execute_activity(...)
except ActivityError as e:
    if isinstance(e.cause, ApplicationError):
        print(e.cause.reason, e.cause.details())
    elif isinstance(e.cause, CadenceTimeoutError):
        print(e.cause.timeout_type)
Hold the contract here
# Deprecated aliases kept for backwards compatibility
ActivityFailure = ActivityError
WorkflowFailure = FailureError
SignalExternalWorkflowFailed = SignalExternalWorkflowError
StartChildWorkflowExecutionFailed = StartChildWorkflowExecutionError
⚠️ Cross-PR: Merging #199 drops data_converter and uses removed error names

📄 cadence/_internal/workflow/statemachine/child_workflow_execution_state_machine.py:139-153 📄 cadence/_internal/workflow/statemachine/activity_state_machine.py:24-29 📄 cadence/_internal/workflow/statemachine/decision_manager.py:129-135 📄 cadence/_internal/workflow/workflow_engine.py:65 📄 cadence/error.py:246-260

Breaks when this PR and #199 both merge, and the one that lands second keeps #199's version of the state-machine construction and failure handlers. The breakage shows up as soon as an activity or child workflow fails, or the module is imported.

How

  1. This PR requires data_converter in ActivityStateMachine(request, completed, data_converter) (activity_state_machine.py:24-29). DecisionManager.__init__ gets it as an extra argument (decision_manager.py:129-135), and WorkflowEngine passes info.data_converter (workflow_engine.py:65).
  2. #199 rewrites DecisionManager.__init__(event_loop) and the schedule_activity / schedule_child_workflow call sites. They build ActivityStateMachine(attrs, future) and ChildWorkflowExecutionStateMachine(attrs, execution, result) with no data_converter.
  3. #199's handle_failed / handle_canceled / handle_timeout / handle_initiation_failed still raise ActivityFailure, ChildWorkflowExecutionCanceled and the other old names. cadence/error.py in this PR no longer defines any of them, and it adds no aliases either (only ActivityError, ChildWorkflowError, StartChildWorkflowExecutionError, etc. at error.py:246-320). Keeping #199's lines therefore fails at import, or with a TypeError on construction. Keeping only part of them means ApplicationError details are never decoded and the parent-requested-cancel force_cancel path is lost.

Fix

  • This PR: nothing needed beyond keeping the DecisionFuture/ChildWorkflowFutureImpl handling for result if this PR lands second.
  • #199: thread data_converter through DecisionManager into both state machines, and build errors with the cadence/_internal/failure.py factories (activity_failed_error, child_workflow_*_error) instead of the removed exception names. Also update its state-machine tests to the new constructor signatures.

Compared 2be6a55 with #199 at c301d52

💡 Edge Case: Strict UTF-8 decode of child cancel details can crash the decision task

📄 cadence/_internal/workflow/statemachine/child_workflow_execution_state_machine.py:156-157

When the parent has requested cancellation, handle_canceled calls self.result.force_cancel(event.details.data.decode()). This decodes the raw details payload as strict UTF-8. That payload comes from the pluggable DataConverter (or from whichever client canceled the child), so it is not guaranteed to be valid UTF-8; a binary converter such as protobuf or msgpack will produce invalid bytes. When it is not, a UnicodeDecodeError is raised inside the history event handler, which fails the decision task, and replaying the same history fails again every time. The rest of this PR decodes untrusted payload bytes with errors="replace" (see ApplicationError._from_payload). Do the same here, or leave the details out of the cancel message entirely.

Decode leniently so malformed bytes can't raise in the event handler
self.result.force_cancel(
    event.details.data.decode("utf-8", errors="replace")
)
✅ 3 closed
✅ Bug: Activity timeout no longer exposes heartbeat details or last failure

📄 cadence/_internal/failure.py:88-97 📄 cadence/error.py:165-170 📄 tests/cadence/_internal/workflow/statemachine/test_activity_state_machine.py:103-117
Before this PR, an activity timeout produced ActivityFailure(..., failure_details=<last_failure>, heartbeat_details=event.details). The old docstring documented heartbeat_details so workflows could decode the activity's last heartbeat progress. The new activity_timed_out_error reads only event.timeout_type. It drops event.details (the heartbeat payload) and event.last_failure (the last application failure before a retry ended in timeout). CadenceTimeoutError and ActivityError have nowhere to keep them, so workflow code can no longer resume from heartbeat progress or see why earlier attempts failed. The PR description does not list this as one of the breaking changes. The updated state-machine test (test_activity_state_machine.py:106) still builds the event with heartbeat details and last_failure but no longer asserts on either. Suggested fix: carry them on the timeout error, e.g. a heartbeat_details payload or decoder on CadenceTimeoutError, plus the last failure as an ApplicationError (or as its __cause__).

✅ Bug: Deprecated child-workflow aliases now catch unrelated failures

📄 cadence/error.py:366-380 📄 tests/cadence/test_error.py:39-50
ChildWorkflowExecutionCanceled, ChildWorkflowExecutionTimedOut, ChildWorkflowExecutionTerminated and ChildWorkflowExecutionFailed all now resolve to the same ChildWorkflowError. Existing code like except ChildWorkflowExecutionTimedOut: <retry/fallback> or except ChildWorkflowExecutionCanceled: return keeps importing without error, but it now also catches child failures, terminations and timeouts. That silently swallows or misroutes errors the old handler never meant to handle. The old canary in tests/canary/cancellation.py had exactly this except (CancelledError, ChildWorkflowExecutionCanceled): return pattern. On top of that, a real cancellation is now asyncio.CancelledError, so the Canceled alias never matches the case it is named for. These aliases make broken code look compatible, which is worse than an ImportError. Suggested fix: alias only the true renames (ActivityFailure, WorkflowFailure, SignalExternalWorkflowFailed, StartChildWorkflowExecutionFailed), and have __getattr__ raise an AttributeError with migration guidance for the removed subtypes.

✅ Quality: ApplicationError docstring references removed ChildWorkflowExecutionFailed

📄 cadence/error.py:93-96
The ApplicationError docstring says workflow code receives "an :class:ActivityError or :class:ChildWorkflowExecutionFailed". This PR removes that class, keeping it only as a deprecated alias of ChildWorkflowError. Readers are pointed at a name that now raises a DeprecationWarning, and the Sphinx cross-reference no longer resolves. It should reference ChildWorkflowError.

🤖 Prompt for agents
Code Review: Standardizes the Python client exception hierarchy under `CadenceError`, adds `ApplicationError` for explicit failure reasons and details, and adds `cadence/_internal/failure.py` for converting exceptions to and from `Failure` messages. Changes requested before merge: strict UTF-8 decoding of child cancel details in `child_workflow_execution_state_machine.py` can fail the decision task on non-UTF-8 payloads, and the Cadence-Docs Python error-handling guide still references removed error classes such as `ActivityFailure` and `SignalFailure`, which will raise `ImportError`. The deprecated aliases promised in the PR description are also missing from `cadence/error.py`. Merging with #199 will also break unless `data_converter` is threaded through `DecisionManager` and #199 builds errors with the new `failure.py` factories.

1. 💡 Edge Case: Strict UTF-8 decode of child cancel details can crash the decision task
   Files: cadence/_internal/workflow/statemachine/child_workflow_execution_state_machine.py:156-157

   When the parent has requested cancellation, `handle_canceled` calls `self.result.force_cancel(event.details.data.decode())`. This decodes the raw details payload as strict UTF-8. That payload comes from the pluggable `DataConverter` (or from whichever client canceled the child), so it is not guaranteed to be valid UTF-8; a binary converter such as protobuf or msgpack will produce invalid bytes. When it is not, a `UnicodeDecodeError` is raised inside the history event handler, which fails the decision task, and replaying the same history fails again every time. The rest of this PR decodes untrusted payload bytes with `errors="replace"` (see `ApplicationError._from_payload`). Do the same here, or leave the details out of the cancel message entirely.

   Fix (Decode leniently so malformed bytes can't raise in the event handler):
   self.result.force_cancel(
       event.details.data.decode("utf-8", errors="replace")
   )

2. ⚠️ Cross-Repo: Cadence-Docs Python error guide still uses removed error classes
   Files: cadence/error.py:246-260, cadence/error.py:57-62

   **Fix here and in [`cadence-workflow/Cadence-Docs`](https://github.com/cadence-workflow/Cadence-Docs)**
   
   **Breaks in:** [`cadence-workflow/Cadence-Docs`](https://github.com/cadence-workflow/Cadence-Docs) - [`docs/07-python-client/09-error-handling.md`](https://github.com/cadence-workflow/Cadence-Docs/blob/HEAD/docs/07-python-client/09-error-handling.md) - `Python error handling guide (Activity failures, Child workflow failures, Signal failures sections and Error reference table)`
   
   The Python error-handling guide in cadence-workflow/Cadence-Docs (`docs/07-python-client/09-error-handling.md`) imports and catches `ActivityFailure`, `StartChildWorkflowExecutionFailed`, `ChildWorkflowExecution{Failed,Canceled,TimedOut,Terminated}`, `SignalExternalWorkflowFailed`, `WorkflowFailure` and `SignalFailure` from `cadence.error`. This PR removes or renames all of these to `ActivityError`, `ChildWorkflowError` (with a `cause`), `StartChildWorkflowExecutionError`, `SignalExternalWorkflowError` and `FailureError`, and `cadence/error.py` defines no aliases for the old names. So every snippet in the guide now raises `ImportError`. The guide's statement that the five child-workflow classes are subclasses of `ChildWorkflowError` is also no longer true: a failure now arrives as a `ChildWorkflowError` whose `cause` is an `ApplicationError`, `CadenceTimeoutError`, `TerminatedError` or `WorkflowCanceledError`, and cancellation the parent requested surfaces as `asyncio.CancelledError`. The break is intentional, but the docs need a coordinated update. The PR description also promises aliases that the code does not provide.
   
   **Here:** Add the deprecated aliases the PR description promises so the old imports keep resolving during migration, and link the docs update from the release notes.
   
   **There:** Rewrite the Activity, Child workflow and Signal failure sections and the Error reference table in docs/07-python-client/09-error-handling.md for the new hierarchy: catch ActivityError / ChildWorkflowError and inspect `.cause`, use StartChildWorkflowExecutionError.failure_cause and SignalExternalWorkflowError, drop SignalFailure, and document ApplicationError and asyncio.CancelledError for cancellation the workflow itself requested.
   
   In cadence-workflow/Cadence-Docs - `docs/07-python-client/09-error-handling.md`:
   
   ````
   from cadence.error import ActivityError, ApplicationError, CadenceTimeoutError
   
   try:
       await workflow.execute_activity(...)
   except ActivityError as e:
       if isinstance(e.cause, ApplicationError):
           print(e.cause.reason, e.cause.details())
       elif isinstance(e.cause, CadenceTimeoutError):
           print(e.cause.timeout_type)
   ````

   Fix (Hold the contract here):
   # Deprecated aliases kept for backwards compatibility
   ActivityFailure = ActivityError
   WorkflowFailure = FailureError
   SignalExternalWorkflowFailed = SignalExternalWorkflowError
   StartChildWorkflowExecutionFailed = StartChildWorkflowExecutionError

3. ⚠️ Cross-PR: Merging [#199](<https://github.com/cadence-workflow/cadence-python-client/pull/199>) drops data_converter and uses removed error names
   Files: cadence/_internal/workflow/statemachine/child_workflow_execution_state_machine.py:139-153, cadence/_internal/workflow/statemachine/activity_state_machine.py:24-29, cadence/_internal/workflow/statemachine/decision_manager.py:129-135, cadence/_internal/workflow/workflow_engine.py:65, cadence/error.py:246-260

   **Breaks when** this PR and [#199](<https://github.com/cadence-workflow/cadence-python-client/pull/199>) both merge, and the one that lands second keeps [#199](<https://github.com/cadence-workflow/cadence-python-client/pull/199>)'s version of the state-machine construction and failure handlers. The breakage shows up as soon as an activity or child workflow fails, or the module is imported.
   
   **How**
   
   1. This PR requires `data_converter` in `ActivityStateMachine(request, completed, data_converter)` (`activity_state_machine.py:24-29`). `DecisionManager.__init__` gets it as an extra argument (`decision_manager.py:129-135`), and `WorkflowEngine` passes `info.data_converter` (`workflow_engine.py:65`).
   2. [#199](<https://github.com/cadence-workflow/cadence-python-client/pull/199>) rewrites `DecisionManager.__init__(event_loop)` and the `schedule_activity` / `schedule_child_workflow` call sites. They build `ActivityStateMachine(attrs, future)` and `ChildWorkflowExecutionStateMachine(attrs, execution, result)` with no `data_converter`.
   3. [#199](<https://github.com/cadence-workflow/cadence-python-client/pull/199>)'s `handle_failed` / `handle_canceled` / `handle_timeout` / `handle_initiation_failed` still raise `ActivityFailure`, `ChildWorkflowExecutionCanceled` and the other old names. `cadence/error.py` in this PR no longer defines any of them, and it adds no aliases either (only `ActivityError`, `ChildWorkflowError`, `StartChildWorkflowExecutionError`, etc. at `error.py:246-320`). Keeping [#199](<https://github.com/cadence-workflow/cadence-python-client/pull/199>)'s lines therefore fails at import, or with a TypeError on construction. Keeping only part of them means `ApplicationError` details are never decoded and the parent-requested-cancel `force_cancel` path is lost.
   
   **Fix**
   
   - **This PR:** nothing needed beyond keeping the `DecisionFuture`/`ChildWorkflowFutureImpl` handling for `result` if this PR lands second.
   - **[#199](<https://github.com/cadence-workflow/cadence-python-client/pull/199>):** thread `data_converter` through `DecisionManager` into both state machines, and build errors with the `cadence/_internal/failure.py` factories (`activity_failed_error`, `child_workflow_*_error`) instead of the removed exception names. Also update its state-machine tests to the new constructor signatures.
   
   _Compared `2be6a55` with [#199](<https://github.com/cadence-workflow/cadence-python-client/pull/199>) at `c301d52`_

Verification PRs 2 inspected

Functional validation No results

Rules No rules evaluated

Broader impact PRs 2 inspected
Cross-PR 2 PRs inspected 1 open impact

PR 199: feat!: Return Futures Instead of Coroutines Inspected
PR 182: feat(observability): replayable logger and metrics Inspected

For more verification details, comment gitar display:verbose.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing compact verification results.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

This branch has not been deployed

No deployments
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