Skip to content

[Bug]: error_type on CallableRuntimeError is lost when using ctx.parallel() — double-wrapped by ErrorObject.from_exception() #297

Description

@Iamrodos

Expected Behavior

When a custom exception is raised inside a ctx.parallel() branch, I'd expect error_type on the resulting CallableRuntimeError (from batch_result.throw_if_error()) to be my original exception's class name — the same way it works with ctx.run_in_child_context(), where error_type correctly preserves the original class name.

try:
    result.throw_if_error()
except CallableRuntimeError as e:
    print(e.error_type)  # Expected: 'PermanentFailure'

Actual Behavior

error_type always comes back as 'CallableRuntimeError' instead of my original exception type. It looks like the exception goes through ErrorObject.from_exception() twice:

First wrap (in ChildOperationExecutor.execute(), operation/child.py):

  • My code raises PermanentFailure("Invalid input data")
  • ErrorObject.from_exception(e)ErrorObject(type="PermanentFailure") — correct
  • to_callable_runtime_error()CallableRuntimeError(error_type="PermanentFailure") — correct

Second wrap (in ConcurrentExecutor._create_result(), concurrency/executor.py):

  • ErrorObject.from_exception(executable.error) is called on the already-wrapped CallableRuntimeError
  • type(exception).__name__ is now CallableRuntimeError, so the original error_type="PermanentFailure" is lost

I'm wondering if I'm doing something wrong, or if this is a gap in how exceptions flow through parallel branches?

Steps to Reproduce

  1. Define a custom exception and a durable function that uses ctx.parallel():
class PermanentFailure(Exception):
    pass

def my_handler(ctx):
    def branch1(child_ctx):
        raise PermanentFailure("Invalid input data")

    result = ctx.parallel([branch1])
    result.throw_if_error()
  1. Catch the CallableRuntimeError and inspect error_type:
try:
    result.throw_if_error()
except CallableRuntimeError as e:
    print(e.error_type)  # Returns 'CallableRuntimeError', not 'PermanentFailure'
  1. Compare with ctx.run_in_child_context() which correctly preserves error_type='PermanentFailure' (only one wrap).

SDK Version

1.3.0

Python Version

3.14

Is this a regression?

No

Last Working Version

No response

Additional Context

Possible fix? Would it make sense for ErrorObject.from_exception() to check if the exception is already a CallableRuntimeError and preserve its error_type?

@classmethod
def from_exception(cls, exception: Exception) -> ErrorObject:
    if isinstance(exception, CallableRuntimeError) and exception.error_type:
        return cls(
            message=str(exception),
            type=exception.error_type,  # Preserve original type
            data=exception.data,
            stack_trace=exception.stack_trace,
        )
    return cls(
        message=str(exception),
        type=type(exception).__name__,
        data=None,
        stack_trace=None,
    )

Workaround: Using serial ctx.run_in_child_context() calls instead of ctx.parallel(), which only wraps once and preserves error_type.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No fields configured for Bug.

    Projects

    Status
    Backlog

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions