temporalio/temporal · error

ErrDuplicateRegistration

ErrDuplicateRegistration

Error message

%w: command handler for %v: %v

What it means

The CHASM workflow Registry.Register rejects a Library whose command handler map contains a command type already registered by a previously registered library. The error wraps ErrDuplicateRegistration and names the conflicting command type plus the existing handler. Registration is expected to happen once, single-threaded, at process init.

Source

Thrown at chasm/lib/workflow/registry.go:47

}

// NewRegistry creates a new [Registry].
func NewRegistry() *Registry {
	return &Registry{
		commandHandlers:          make(map[enumspb.CommandType]CommandHandler),
		eventDefinitions:         make(map[enumspb.EventType]EventDefinition),
		eventDefinitionsByGoType: make(map[reflect.Type]EventDefinition),
	}
}

// Register registers all command handlers and event definitions from a [Library].
// Returns an [ErrDuplicateRegistration] if a handler or definition is already registered.
// All registration is expected to happen in a single thread on process initialization.
func (r *Registry) Register(lib Library) error {
	for t, handler := range lib.CommandHandlers() {
		if existing, ok := r.commandHandlers[t]; ok {
			return fmt.Errorf("%w: command handler for %v: %v", ErrDuplicateRegistration, t, existing)
		}
		r.commandHandlers[t] = handler
	}
	for _, def := range lib.EventDefinitions() {
		if existing, ok := r.eventDefinitions[def.Type()]; ok {
			return fmt.Errorf("%w: event handler for %v: %v", ErrDuplicateRegistration, def.Type(), existing)
		}
		goType := reflect.TypeOf(def)
		for goType.Kind() == reflect.Pointer {
			goType = goType.Elem()
		}
		if existing, ok := r.eventDefinitionsByGoType[goType]; ok {
			return fmt.Errorf("%w: event definition for Go type %v: %v", ErrDuplicateRegistration, goType, existing)
		}
		r.eventDefinitions[def.Type()] = def
		r.eventDefinitionsByGoType[goType] = def
	}
	return nil
}

View on GitHub (pinned to bde624efd1)

Solutions

  1. Find the duplicate: the message names the command type and existing handler — locate both libraries registering it
  2. Remove one registration or consolidate the two command handlers into a single library
  3. Guard registration with a sync.Once or check-and-skip so init paths that may run twice register only once
  4. If intentional replacement is needed, create a fresh Registry instead of reusing one (no overwrite API exists)

Example fix

// before
registry.Register(libA) // registers command type Foo
registry.Register(libB) // also registers command type Foo -> duplicate
// after
registry.Register(libA)
registry.Register(libBWithoutFoo) // libB no longer defines Foo handler
Defensive patterns

Strategy: validation

Validate before calling

// Guard against double registration before calling Register
var once sync.Once
func registerOnce(r *chasmworkflow.Registry, lib Library) {
    once.Do(func() { _ = r.Register(lib) })
}

Try / catch

// Go
if err := registry.Register(lib); err != nil {
    if errors.Is(err, ErrDuplicateRegistration) {
        logger.Warn("library already registered; skipping", "err", err)
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: Calling Registry.Register(lib) where lib.CommandHandlers() returns a handler for a command type t that r.commandHandlers already holds — typically by registering two libraries that both define handlers for the same command type, or re-registering the same library twice.

Common situations: Two plugins/libraries bundled into the same binary both register the same command type; an init() or registration path runs twice (e.g. library imported in two places, or Register called in both init and main); copy-pasted library code kept the same command type.

Related errors


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