hibiken/asynq · error

task ID cannot be empty

Error message

task ID cannot be empty

What it means

A sentinel-less validation error raised in composeOptions when the taskIDOption value is blank (only whitespace). The client refuses to enqueue a task whose TaskID option carries an empty string, since an empty ID would be meaningless for conflict detection.

Solutions

  1. Validate the ID is non-blank before passing it to asynq.TaskID.
  2. Skip the TaskID option entirely when no ID is available.
  3. Fix the upstream source (struct field, DB lookup) producing the empty ID.

Example fix

// before
client.Enqueue(task, asynq.TaskID(cfg.TaskID))
// after
opts := []asynq.Option{}
if strings.TrimSpace(cfg.TaskID) != "" {
    opts = append(opts, asynq.TaskID(cfg.TaskID))
}
client.Enqueue(task, opts...)
Defensive patterns

Strategy: validation

Validate before calling

if strings.TrimSpace(id) == "" { return errors.New("task ID must be non-empty") }

Type guard

func validTaskID(id string) bool { return strings.TrimSpace(id) != "" }

Try / catch

// Validation-preferred; if hit anyway:
if err != nil && strings.Contains(err.Error(), "task ID cannot be empty") { /* fix caller data */ }

Prevention

When it happens

Trigger: Calling client.Enqueue(task, asynq.TaskID("")) or TaskID(" "), typically because a variable holding the ID was empty at call time.

Common situations: Building TaskID from a struct field or DB column that was unset/empty; string formatting that produced an empty ID; forgetting to validate user-supplied IDs before enqueueing.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at client.go:295

		timeout:   0, // do not set to defaultTimeout here
		deadline:  time.Time{},
		processAt: time.Now(),
		headers:   make(map[string]string),
	}
	for _, opt := range opts {
		switch opt := opt.(type) {
		case retryOption:
			res.retry = int(opt)
		case queueOption:
			qname := string(opt)
			if err := base.ValidateQueueName(qname); err != nil {
				return option{}, err
			}
			res.queue = qname
		case taskIDOption:
			id := string(opt)
			if isBlank(id) {
				return option{}, errors.New("task ID cannot be empty")
			}
			res.taskID = id
		case timeoutOption:
			res.timeout = time.Duration(opt)
		case deadlineOption:
			res.deadline = time.Time(opt)
		case uniqueOption:
			ttl := time.Duration(opt)
			if ttl < 1*time.Second {
				return option{}, errors.New("Unique TTL cannot be less than 1s")
			}
			res.uniqueTTL = ttl
		case processAtOption:
			res.processAt = time.Time(opt)
		case processInOption:
			res.processAt = time.Now().Add(time.Duration(opt))
		case retentionOption:
			res.retention = time.Duration(opt)

View on GitHub (pinned to d135f1439b)