argoproj/argo-workflows · error

%w: %w

Error message

%w: %w

What it means

util/wait.Backoff wraps k8s.io/apimachinery's ExponentialBackoff to preserve the error returned by the condition function, which ExponentialBackoff otherwise discards when retries are exhausted. When the backoff times out AND the last attempt returned an error, both are joined as 'waitErr: err' via dual %w wrapping, so errors.Is/As work on either.

Source

Thrown at util/wait/backoff.go:20

import (
	"fmt"

	"k8s.io/apimachinery/pkg/util/wait"
)

// Backoff wraps ExponentialBackoff to retain the underlying error,
// which the standard ExponentialBackoff does not preserve.
func Backoff(b wait.Backoff, f func() (bool, error)) error {
	var err error
	waitErr := wait.ExponentialBackoff(b, func() (bool, error) {
		var done bool
		done, err = f()
		return done, nil
	})
	if waitErr != nil {
		if err != nil {
			return fmt.Errorf("%w: %w", waitErr, err)
		}
		return waitErr
	}
	return err
}

View on GitHub (pinned to 35bff19146)

Solutions

  1. Read the wrapped cause after the colon / use errors.Is(err, context.DeadlineExceeded) or errors.Is/As on the underlying error to find the real failure
  2. Fix the root cause reported by the wrapped error (missing resource, RBAC, connectivity)
  3. Increase the wait.Backoff duration/steps if the operation is legitimately slow but eventually succeeds
  4. If the condition is expected to fail indefinitely, handle the timeout explicitly rather than retrying forever

Example fix

// before: losing the cause
err := wait.ExponentialBackoff(b, f) // only "timed out waiting for the condition"
// after: argo's wrapper keeps both
err := waitutil.Backoff(b, f) // "timed out waiting for the condition: configmaps \"x\" not found"
var nf *apierr.StatusError
if errors.As(err, &nf) && apierr.IsNotFound(nf) { /* handle missing resource */ }
Defensive patterns

Strategy: try-catch

Validate before calling

// preflight the resource the backoff waits on
_, err := kube.CoreV1().ConfigMaps(ns).Get(ctx, name, metav1.GetOptions{})
if apierr.IsNotFound(err) { /* create it or fail fast before retrying */ }

Try / catch

err := waitutil.Backoff(retry.DefaultRetry(ctx), cond)
if err != nil {
    var lastErr error
    if unwrapped := errors.Unwrap(err); unwrapped != nil { lastErr = unwrapped }
    if errors.Is(err, wait.ErrWaitTimeout) {
        return fmt.Errorf("condition not met after retries; last cause: %v", lastErr)
    }
    return err
}

Prevention

When it happens

Trigger: The condition func passed to waitutil.Backoff keeps returning (false, err) until the wait.Backoff attempts are exhausted — ExponentialBackoff returns ErrWaitTimeout (or a context error) while the underlying last err is non-nil, producing 'timed out waiting for the condition: <your error>'.

Common situations: Waiting for a ConfigMap/CR to appear that never does (e.g. artifact-repositories ConfigMap missing) until retries run out; API server returning persistent errors during a transient retry loop; backoff too short for a slow operation.

Related errors


AI-assisted analysis of argoproj/argo-workflows@35bff19146 (2026-09-03). Data as JSON: /api/errors/f834260b659b1de3. Report an issue: GitHub.