temporalio/temporal · error

failed to deserialize OperationError: %w

Error message

failed to deserialize OperationError: %w

What it means

Same converter path as the HandlerError case but for failures with metadata type "nexus.OperationError": the JSON in f.Details must unmarshal into serializedOperationError (containing the operation State). If it does not, this error is returned because the failure cannot be represented as a nexus.OperationError.

Source

Thrown at common/nexus/nexusrpc/failure_converter.go:161

			he := &nexus.HandlerError{
				Message:         f.Message,
				StackTrace:      f.StackTrace,
				Type:            nexus.HandlerErrorType(se.Type),
				RetryBehavior:   se.RetryBehavior(),
				OriginalFailure: &f,
			}
			if f.Cause != nil {
				he.Cause, err = e.FailureToError(*f.Cause)
				if err != nil {
					return nil, err
				}
			}
			return he, nil
		case "nexus.OperationError":
			var se serializedOperationError
			err := json.Unmarshal(f.Details, &se)
			if err != nil {
				return nil, fmt.Errorf("failed to deserialize OperationError: %w", err)
			}
			oe := &nexus.OperationError{
				Message:         f.Message,
				StackTrace:      f.StackTrace,
				State:           nexus.OperationState(se.State),
				OriginalFailure: &f,
			}
			if f.Cause != nil {
				oe.Cause, err = e.FailureToError(*f.Cause)
				if err != nil {
					return nil, err
				}
			}
			return oe, nil
		}
	}
	// Note that the original failure cause is retained on the FailureError's failure object.
	fe := &nexus.FailureError{Failure: f}

View on GitHub (pinned to bde624efd1)

Solutions

  1. Inspect f.Metadata and f.Details to confirm the actual wire format of the failure.
  2. Regenerate/produce failures with the matching SDK version so Details uses the expected JSON schema.
  3. Fix any middleware that rewrites Details to preserve the original JSON bytes.
  4. Fall back to handling the raw nexus.Failure when typed conversion fails.

Example fix

// before
oe := err.(*nexus.OperationError) // hard cast after conversion fails
// after
oe, ok := err.(*nexus.OperationError)
if !ok {
    return fmt.Errorf("operation failed: %s", err.Error())
}
Defensive patterns

Strategy: type-guard

Validate before calling

if failure.Metadata["type"] == "nexus.OperationError" && !json.Valid(failure.Details) {
    log.Printf("malformed OperationError details; treating as generic failure")
}

Type guard

func isOperationError(err error) (*nexus.OperationError, bool) {
    oe, ok := err.(*nexus.OperationError)
    return oe, ok
}

Try / catch

err, cerr := converter.FailureToError(failure)
if cerr != nil {
    // fall back: inspect State from the raw failure if present, else generic
    return errors.New(failure.Message)
}
var oe *nexus.OperationError
if errors.As(err, &oe) && oe.State == nexus.OperationStateFailed {
    // handle failed operation
}

Prevention

When it happens

Trigger: Calling FailureToError on a failure tagged "nexus.OperationError" whose Details JSON is invalid or has the wrong fields (e.g. missing/mistyped "state"), typically produced by an incompatible writer.

Common situations: SDK version skew where the serialized OperationError schema changed; manually built failures in tests; failures round-tripped through systems that re-encode Details as non-JSON (e.g. base64 proto).

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/cd7a333d495c5735. Report an issue: GitHub.