hibiken/asynq · critical

asynq: cannot start uninitialized PeriodicTaskManager; use…

Error message

asynq: cannot start uninitialized PeriodicTaskManager; use NewPeriodicTaskManager to initialize

What it means

PeriodicTaskManager.Start panics when the manager's internal scheduler (mgr.s) or provider (mgr.p) is nil, i.e. the struct was created with plain &PeriodicTaskManager{} or via a zero-value composite literal instead of the required NewPeriodicTaskManager constructor. The library requires construction via NewPeriodicTaskManager, which wires the scheduler and provider, so Start on an uninitialized instance is a programming error.

Solutions

  1. Construct with asynq.NewPeriodicTaskManager(asynq.PeriodicTaskManagerOpts{RedisConnOpt: ..., Provider: ...}) and use the returned pointer
  2. Use mgr.Run() instead of calling Start manually if you want start+stop handled together
  3. Check that the constructor's error return is handled and the non-nil result is stored
  4. Never copy the PeriodicTaskManager value; always pass and store the *PeriodicTaskManager pointer

Example fix

// before
var mgr asynq.PeriodicTaskManager
mgr.Start()

// after
mgr, err := asynq.NewPeriodicTaskManager(asynq.PeriodicTaskManagerOpts{
    RedisConnOpt: asynq.RedisClientOpt{Addr: ":6379"},
    Provider:     myConfigProvider,
})
if err != nil { log.Fatal(err) }
if err := mgr.Run(); err != nil { log.Fatal(err) }
Defensive patterns

Strategy: validation

Validate before calling

if mgr == nil {
    return errors.New("PeriodicTaskManager not constructed; use NewPeriodicTaskManager")
}
if err := mgr.Run(); err != nil { log.Fatal(err) }

Type guard

func managerReady(mgr *asynq.PeriodicTaskManager) bool {
    return mgr != nil
}

Prevention

When it happens

Trigger: Calling Start on a PeriodicTaskManager built as &asynq.PeriodicTaskManager{} or asynq.PeriodicTaskManager{}; declaring the manager as a struct field and using it before assigning the result of NewPeriodicTaskManager; copying the manager value so internal fields end up nil.

Common situations: DI/manual wiring skipping the constructor; refactoring where NewPeriodicTaskManager's error return was dropped and the nil manager was kept; embedding the manager by value in another struct causing a partial copy.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of hibiken/asynq@d135f1439b (2026-09-07). Data as JSON: /api/errors/90aafee46a556852. Report an issue: GitHub.

Appendix: source

Thrown at periodic_task_manager.go:122

	if c == nil {
		return fmt.Errorf("PeriodicTaskConfig cannot be nil")
	}
	if c.Task == nil {
		return fmt.Errorf("PeriodicTaskConfig.Task cannot be nil")
	}
	if c.Cronspec == "" {
		return fmt.Errorf("PeriodicTaskConfig.Cronspec cannot be empty")
	}
	return nil
}

// Start starts a scheduler and background goroutine to sync the scheduler with the configs
// returned by the provider.
//
// Start returns any error encountered at start up time.
func (mgr *PeriodicTaskManager) Start() error {
	if mgr.s == nil || mgr.p == nil {
		panic("asynq: cannot start uninitialized PeriodicTaskManager; use NewPeriodicTaskManager to initialize")
	}
	if err := mgr.initialSync(); err != nil {
		return fmt.Errorf("asynq: %w", err)
	}
	if err := mgr.s.Start(); err != nil {
		return fmt.Errorf("asynq: %w", err)
	}
	mgr.wg.Add(1)
	go func() {
		defer mgr.wg.Done()
		ticker := time.NewTicker(mgr.syncInterval)
		for {
			select {
			case <-mgr.done:
				mgr.s.logger.Debugf("Stopping syncer goroutine")
				ticker.Stop()
				return
			case <-ticker.C:

View on GitHub (pinned to d135f1439b)