temporalio/temporal · error

task type is already registered

Error message

task type %s is already registered

What it means

registerTask found that r.rtByGoType already contains a task with this exact Go type — the same task implementation was registered twice. The registry allows only one registration per Go type regardless of the name used.

Solutions

  1. Delete the second registration of the same task Go type.
  2. Centralize task registration in one init/bootstrap function.
  3. Use a fresh Registry instance for isolated test cases instead of re-registering into a shared one.

Example fix

// before
chasm.RegisterTask[EmailTask, OrderImpl](reg, "email", opts)
chasm.RegisterTask[EmailTask, OrderImpl](reg, "email2", opts)
// after
chasm.RegisterTask[EmailTask, OrderImpl](reg, "email", opts)
Defensive patterns

Strategy: validation

Validate before calling

if _, ok := registryLookupTask(reflect.TypeOf(MyTask{})); ok {
	return errors.New("task already registered; skip duplicate RegisterTask call")
}

Try / catch

if err := chasm.RegisterTask[MyTask, C](reg, name, opts); err != nil && strings.Contains(err.Error(), "is already registered") {
	return nil
}

Prevention

When it happens

Trigger: Calling chasm.RegisterTask twice with the same task Go type (even with different names/options); duplicate init() paths registering the same task; test setup re-registering tasks on a shared registry.

Common situations: Task registered in both a library's init and the application's bootstrap; re-running a bootstrap function during tests; accidental duplicated registration line after a merge.

Related errors


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

Appendix: source

Thrown at chasm/registry.go:319

	fqn, id, err := rt.registerToLibrary(lib)
	if err != nil {
		return err
	}

	if _, ok := r.rtByFqn[fqn]; ok {
		return fmt.Errorf("task %s is already registered", fqn)
	}

	if existingTask, ok := r.rtByID[id]; ok {
		return fmt.Errorf("task type ID %d collision between %s and %s", id, fqn, existingTask.fqType())
	}

	if !(rt.goType.Kind() == reflect.Struct ||
		(rt.goType.Kind() == reflect.Pointer && rt.goType.Elem().Kind() == reflect.Struct)) {
		return fmt.Errorf("task type %s must be struct or pointer to struct", rt.goType.String())
	}
	if _, ok := r.rtByGoType[rt.goType]; ok {
		return fmt.Errorf("task type %s is already registered", rt.goType.String())
	}
	if !(rt.componentGoType.Kind() == reflect.Interface ||
		(rt.componentGoType.Kind() == reflect.Struct ||
			(rt.componentGoType.Kind() == reflect.Pointer && rt.componentGoType.Elem().Kind() == reflect.Struct)) &&
			rt.componentGoType.AssignableTo(reflect.TypeFor[Component]())) {
		return fmt.Errorf("component type %s must be and interface or struct that implements Component interface", rt.componentGoType.String())
	}

	r.rtByFqn[fqn] = rt
	r.rtByID[id] = rt
	r.rtByGoType[rt.goType] = rt
	return nil
}

func (r *Registry) validateName(n string) error {
	if n == "" {
		return errors.New("name must not be empty")
	}

View on GitHub (pinned to bde624efd1)