temporalio/temporal · error

invalid state

Error message

invalid state

What it means

ContextImpl.errorByState maps the shard context's lifecycle state to an error for callers during an operation: uninitialized/acquiring → ErrShardStatusUnknown, acquired → nil, stopping/stopped → shard-closed error. The default branch panics because every defined contextState should be covered; hitting it means the state variable holds an undefined value. It is an exhaustiveness invariant, not a caller-recoverable condition.

Source

Thrown at service/history/shard/context_impl.go:1142

}

func (s *ContextImpl) getRangeIDLocked() int64 {
	return s.shardInfo.GetRangeId()
}

func (s *ContextImpl) errorByState() error {
	s.stateLock.Lock()
	defer s.stateLock.Unlock()

	switch s.state {
	case contextStateInitialized, contextStateAcquiring:
		return ErrShardStatusUnknown
	case contextStateAcquired:
		return nil
	case contextStateStopping, contextStateStopped:
		return s.newShardClosedErrorWithShardID()
	default:
		panic("invalid state")
	}
}

func (s *ContextImpl) errorByNamespaceStateLocked(
	namespaceName namespace.Name,
	workflowID string,
) error {
	if s.handoverTracker.IsInHandover(namespaceName, workflowID) {
		return consts.ErrNamespaceHandover
	}
	return nil
}

func (s *ContextImpl) generateTaskIDLocked() (int64, error) {
	taskKey, err := s.taskKeyManager.generateTaskKey(tasks.CategoryTransfer)
	if err != nil {
		return -1, err
	}

View on GitHub (pinned to bde624efd1)

Solutions

  1. Add the missing contextState case to errorByState, returning the appropriate error
  2. Grep contextState* constants and audit every switch over s.state for exhaustiveness
  3. Run the shard controller tests under -race to rule out unsynchronized state writes
  4. If hit on a stock build, capture the shard ID and stack and file an issue with temporalio/temporal

Example fix

// before
case contextStateStopping, contextStateStopped:
  return s.newShardClosedErrorWithShardID()
default:
  panic("invalid state")
// after
case contextStateStopping, contextStateStopped:
  return s.newShardClosedErrorWithShardID()
case contextStateLoading: // newly added state
  return ErrShardStatusUnknown
default:
  panic("invalid state")
Defensive patterns

Strategy: validation

Validate before calling

// On shard-context errors, check the returned sentinel errors instead of panics:
err := ctx.UpdateWorkflowExecution(...)
switch {
case err == nil:
  // proceed
case errors.Is(err, shard.ErrShardStatusUnknown):
  // shard not yet acquired; retry
case shard.IsShardClosed(err):
  // shard ownership lost; re-lookup shard
}

Type guard

func shardUsable(ctx shard.Context) bool {
  // probe with a cheap call that maps state to error rather than panicking
  return ctx.ErrorByState() == nil // via public surface; panics only on corrupt state
}

Try / catch

func safeUpdate(ctx shard.Context, req *historyservice.UpdateWorkflowExecutionRequest) (resp interface{}, err error) {
  defer func() { if r := recover(); r != nil { err = fmt.Errorf("shard context invalid state: %v", r) } }()
  return ctx.UpdateWorkflowExecution(context.Background(), req)
}

Prevention

When it happens

Trigger: A new contextState constant added to shard context without updating errorByState's switch; a race or memory bug corrupting s.state; direct writes to the state field somewhere bypassing the state machine transitions.

Common situations: Developing/forking temporal-server's shard controller and adding states (e.g. contextStateLoading) without updating all switches; concurrency bugs surfaced only under heavy shard ownership transfer load.

Related errors


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