docker/cli · error

failed to wait on tasks of stack

Error message

failed to wait on tasks of stack: %s: %w

What it means

Raised during `docker stack rm` (when not detached) if waitOnTasks fails for a stack. The wrapped %w carries the error from waitOnTasks (which itself wraps getStackTasks failures). It indicates removal did not complete cleanly within the wait loop, usually because task listing or task termination could not be observed.

Solutions

  1. Read the wrapped error to see whether it's an API/RPC failure or a task-listing issue.
  2. Re-run `docker stack rm <stack>` (the resources still pending will be reprocessed).
  3. Check manager health with `docker node ls` and that the node is reachable.
  4. If tasks are stuck, inspect them with `docker stack ps <stack>` and force-remove obstructing services.
  5. Use `--detach` to skip the wait if removal was issued but convergence can't be observed.

Example fix

// before: blocking remove fails waiting on tasks
docker stack rm mystack
// after: detach from the wait, then verify
docker stack rm --detach mystack
docker stack ps mystack  # should eventually show nothing
Defensive patterns

Strategy: retry

Validate before calling

// Pre-check manager reachability before a blocking remove
info, err := c.Info(ctx, client.InfoOptions{})
if err != nil { return fmt.Errorf("manager unreachable: %w", err) }
if !info.Info.Swarm.ControlAvailable { return errors.New("not a swarm manager") }

Try / catch

// Retry stack rm on transient wait failures; fall back to --detach
for attempt := 0; attempt < 3; attempt++ {
    if err := stackRm(ctx, cli, opts); err == nil {
        return nil
    } else if !strings.Contains(err.Error(), "failed to wait on tasks") {
        return err
    }
}
// last resort: detach from the wait
opts.detach = true
return stackRm(ctx, cli, opts)

Prevention

When it happens

Trigger: Running `docker stack rm <stack>` without --detach; after services/networks/etc. are removed, waitOnTasks at remove.go:90 polls getStackTasks until all tasks reach a terminal state, and if that poll fails (e.g. RPC error to the manager) the error is wrapped at remove.go:92.

Common situations: Manager node becoming unreachable mid-removal; the wait loop hitting an API error; tasks stuck in a non-terminal state due to node loss; the daemon returning errors during TaskList while resources are being torn down.

Related errors


AI-assisted analysis of docker/cli@4f84911bfe (2026-08-07). Data as JSON: /api/errors/948346a1a1fdd7ea. Report an issue: GitHub.

Appendix: source

Thrown at cli/command/stack/remove.go:92

			_, _ = fmt.Fprintln(dockerCli.Err(), "Nothing found in stack:", namespace)
			continue
		}

		// TODO(thaJeztah): change this "hasError" boolean to return a (multi-)error for each of these functions instead.
		hasError := removeServices(ctx, dockerCli, services.Items)
		hasError = removeSecrets(ctx, dockerCli, secrets.Items) || hasError
		hasError = removeConfigs(ctx, dockerCli, configs.Items) || hasError
		hasError = removeNetworks(ctx, dockerCli, networks.Items) || hasError

		if hasError {
			errs = append(errs, errors.New("failed to remove some resources from stack: "+namespace))
			continue
		}

		if !opts.detach {
			err = waitOnTasks(ctx, apiClient, namespace)
			if err != nil {
				errs = append(errs, fmt.Errorf("failed to wait on tasks of stack: %s: %w", namespace, err))
			}
		}
	}
	return errors.Join(errs...)
}

func sortServiceByName(services []swarm.Service) func(i, j int) bool {
	return func(i, j int) bool {
		return services[i].Spec.Name < services[j].Spec.Name
	}
}

func removeServices(ctx context.Context, dockerCLI command.Cli, services []swarm.Service) bool {
	var hasError bool
	sort.Slice(services, sortServiceByName(services))
	for _, service := range services {
		_, _ = fmt.Fprintln(dockerCLI.Out(), "Removing service", service.Spec.Name)
		if _, err := dockerCLI.Client().ServiceRemove(ctx, service.ID, client.ServiceRemoveOptions{}); err != nil {

View on GitHub (pinned to 4f84911bfe)