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
- 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
- Fix the root cause reported by the wrapped error (missing resource, RBAC, connectivity)
- Increase the wait.Backoff duration/steps if the operation is legitimately slow but eventually succeeds
- 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
- Always inspect the full error string — the cause after the colon is the real problem
- Size the wait.Backoff (steps/Duration/Cap) generously for slow remote systems
- Preflight-existence-check resources before entering retry loops
- Log the underlying condition-function error on each attempt for observability
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
- requires either node field selector or workflow
- resolve UID: %w
- supervisor presumed dead: status marker never appeared withi
- operation failed after reconnection: %w
- plugin %s context cancelled while waiting for socket at %q:
AI-assisted analysis of argoproj/argo-workflows@35bff19146 (2026-09-03).
Data as JSON: /api/errors/f834260b659b1de3.
Report an issue: GitHub.