temporalio/temporal · error

failed to serialize handler result: %w

Error message

failed to serialize handler result: %w

What it means

writeResult serializes a Nexus handler's return value using the configured Serializer when it is not already a *nexus.Content. If serialization fails (the result type is not supported by the serializer), the handler writes a failure response with this error rather than the result.

Source

Thrown at common/nexus/nexusrpc/server.go:69

	BaseHTTPHandler
	options HandlerOptions
}

func (h *httpHandler) writeResult(writer http.ResponseWriter, request *http.Request, result any) {
	var reader *nexus.Reader
	if r, ok := result.(*nexus.Reader); ok {
		// Close the request body in case we error before sending the HTTP request (which may double close but
		// that's fine since we ignore the error).
		// nolint:errcheck // ignore error on close
		defer r.Close()
		reader = r
	} else {
		content, ok := result.(*nexus.Content)
		if !ok {
			var err error
			content, err = h.options.Serializer.Serialize(result)
			if err != nil {
				h.WriteFailure(writer, request, fmt.Errorf("failed to serialize handler result: %w", err))
				return
			}
		}
		header := maps.Clone(content.Header)
		header["length"] = strconv.Itoa(len(content.Data))

		reader = &nexus.Reader{
			ReadCloser: io.NopCloser(bytes.NewReader(content.Data)),
			Header:     header,
		}
	}

	header := writer.Header()
	addContentHeaderToHTTPHeader(reader.Header, header)
	if reader.ReadCloser == nil {
		return
	}
	if _, err := io.Copy(writer, reader); err != nil {

View on GitHub (pinned to bde624efd1)

Solutions

  1. Log the underlying serializer error to see which value/type failed to serialize.
  2. Convert the handler result to a type the serializer supports (e.g. build a *commonpb.Payload or a *nexus.Content).
  3. Register/choose a Serializer that supports all result types your handlers return.
  4. Ensure custom serializers implement Serialize for every type used in handler signatures.

Example fix

// before
func (h *Handler) Greet(ctx context.Context, input struct{}) (MyStruct, error) { return s, nil } // serializer cannot handle MyStruct
// after
payload, err := proto.Marshal(&s) // convert to supported Payload first
// or return *nexus.Content / *commonpb.Payload from the handler
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the handler's return value is serializable before registering
var _ = func(result any) error {
    _, err := serializer.Serialize(result)
    return err
}

Type guard

func isSerializable(v any) bool {
    if _, ok := v.(*nexus.Content); ok {
        return true
    }
    if _, ok := v.(*commonpb.Payload); ok {
        return true
    }
    return v == nil
}

Try / catch

// Handler side: return serialization errors as handler failures
func (h *Handler) Op(ctx context.Context, in Input) (Output, error) {
    out, err := doWork(in)
    if err != nil {
        return Output{}, nexus.HandlerErrorf(nexus.HandlerErrorTypeInternal, "work failed: %v", err)
    }
    return out, nil // ensure type matches serializer support
}

Prevention

When it happens

Trigger: A Nexus operation handler returns a value that the registered payload serializer cannot serialize — e.g. an arbitrary struct when the serializer only accepts *commonpb.Payload or nil — and the result is written back to the HTTP response.

Common situations: Returning the wrong type from a handler (forgetting to convert a struct to a Payload); swapping in a custom Serializer that does not support the handler's result type; returning an unsupported concrete type from a typed handler whose serialization contract changed.

Related errors


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