temporalio/temporal · error

empty operation token

Error message

empty operation token

What it means

errEmptyOperationToken is a sentinel in common/nexus/nexusrpc/client.go indicating NewOperationHandle received an empty operation token. The token identifies a started asynchronous operation instance; without it the handle cannot address the operation, so it is rejected at construction time.

Source

Thrown at common/nexus/nexusrpc/client.go:43

	// A function for making HTTP requests.
	// Defaults to [http.DefaultClient.Do].
	HTTPCaller func(*http.Request) (*http.Response, error)
	// A [Serializer] to customize client serialization behavior.
	// By default the client handles JSONables, byte slices, and nil.
	Serializer nexus.Serializer
	// A [FailureConverter] to convert a [Failure] instance to and from an [error]. Defaults to
	// [DefaultFailureConverter].
	FailureConverter FailureConverter
}

// User-Agent header set on HTTP requests.
const userAgent = "temporalio/server"

const headerUserAgent = "User-Agent"

var errEmptyOperationName = errors.New("empty operation name")

var errEmptyOperationToken = errors.New("empty operation token")

// UnexpectedResponseError indicates a client encountered something unexpected in the server's response.
type UnexpectedResponseError struct {
	// Error message.
	Message string
	// Optional failure that may have been emedded in the response.
	Failure *nexus.Failure
	// Additional transport specific details.
	// For HTTP, this would include the HTTP response. The response body will have already been read into memory and
	// does not need to be closed.
	Details any
}

// Error implements the error interface.
func (e *UnexpectedResponseError) Error() string {
	return e.Message
}

View on GitHub (pinned to bde624efd1)

Solutions

  1. Ensure the token returned from a successful StartOperation call is propagated to NewOperationHandle
  2. Persist and restore the token (it is often base64-encoded) without dropping it
  3. Guard with errors.Is(err, nexusrpc.ErrEmptyOperationToken) to surface the cause

Example fix

// before
handle, _ := client.NewOperationHandle(ctx, op, savedToken) // savedToken was ""
// after
if savedToken == "" {
    return nil, fmt.Errorf("operation token missing from stored handle")
}
handle, err := client.NewOperationHandle(ctx, op, savedToken)
Defensive patterns

Strategy: validation

Validate before calling

func checkHandleArgs(operationName, token string) error {
    if token == "" { return errors.New("operation token required") }
    return nil
}

Type guard

func hasOperationToken(token string) bool { return token != "" }

Try / catch

handle, err := client.NewOperationHandle(ctx, opName, token)
if errors.Is(err, nexusrpc.ErrEmptyOperationToken) {
    return fmt.Errorf("operation token missing: was the operation started and its token persisted?")
}

Prevention

When it happens

Trigger: Calling NewOperationHandle with operationToken == "", e.g. a token that was never captured from a StartOperation response, or one lost through an empty JSON field when persisting the handle.

Common situations: Storing operation handles and reading back an empty token field; passing through unset variables from a StartOperation result; test coverage of failure conditions (TestNewHandleFailureConditions).

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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