hibiken/asynq · error

PeriodicTaskConfig.Task cannot be nil

Error message

PeriodicTaskConfig.Task cannot be nil

What it means

validatePeriodicTaskConfig returns this error when a PeriodicTaskConfig has a nil Task field. A config without a Task has nothing to enqueue, so the sync loop rejects it. Raised during initialSync/sync for each offending provider-supplied config.

Solutions

  1. Always populate Task with a non-nil *asynq.Task in every config
  2. Add a pre-return validation loop in the provider mirroring validatePeriodicTaskConfig
  3. Fail fast in the provider when a spec cannot be mapped to a Task

Example fix

// before
return []*asynq.PeriodicTaskConfig{
    {Cronspec: "0 6 * * *", Options: []asynq.Option{}}, // Task missing
}, nil
// after
return []*asynq.PeriodicTaskConfig{
    {Task: asynq.NewTask("report:generate", nil), Cronspec: "0 6 * * *"},
}, nil
Defensive patterns

Strategy: validation

Validate before calling

for i, c := range cfgs {
    if c != nil && c.Task == nil {
        return fmt.Errorf("config %d (%s) has nil Task", i, c.Cronspec)
    }
}

Type guard

func hasTask(c *asynq.PeriodicTaskConfig) bool { return c != nil && c.Task != nil }

Try / catch

if err := mgr.Run(); err != nil {
    if strings.Contains(err.Error(), "PeriodicTaskConfig.Task cannot be nil") {
        // fix provider to always set Task
    }
    return err
}

Prevention

When it happens

Trigger: A PeriodicTaskConfigProvider returns a config where only Cronspec (and maybe Options) is set but the Task field is left nil.

Common situations: Constructing configs from persisted spec strings and forgetting to rebuild the asynq.Task; conditional task construction that yields nil on a code path; copy-paste of a config literal missing the Task line.

Related errors


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

Appendix: source

Thrown at periodic_task_manager.go:108

func (c *PeriodicTaskConfig) hash() string {
	h := sha256.New()
	_, _ = h.Write([]byte(c.Cronspec))
	_, _ = h.Write([]byte(c.Task.Type()))
	h.Write(c.Task.Payload())
	opts := stringifyOptions(c.Opts)
	sort.Strings(opts)
	for _, opt := range opts {
		_, _ = h.Write([]byte(opt))
	}
	return fmt.Sprintf("%x", h.Sum(nil))
}

func validatePeriodicTaskConfig(c *PeriodicTaskConfig) error {
	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)
	}

View on GitHub (pinned to d135f1439b)