{"record":{"id":"f834260b659b1de3","repo":"argoproj/argo-workflows","slug":"w-w","errorCode":null,"errorMessage":"%w: %w","messagePattern":"%w: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"util/wait/backoff.go","lineNumber":20,"sourceCode":"\nimport (\n\t\"fmt\"\n\n\t\"k8s.io/apimachinery/pkg/util/wait\"\n)\n\n// Backoff wraps ExponentialBackoff to retain the underlying error,\n// which the standard ExponentialBackoff does not preserve.\nfunc Backoff(b wait.Backoff, f func() (bool, error)) error {\n\tvar err error\n\twaitErr := wait.ExponentialBackoff(b, func() (bool, error) {\n\t\tvar done bool\n\t\tdone, err = f()\n\t\treturn done, nil\n\t})\n\tif waitErr != nil {\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"%w: %w\", waitErr, err)\n\t\t}\n\t\treturn waitErr\n\t}\n\treturn err\n}\n","sourceCodeStart":2,"sourceCodeEnd":26,"githubUrl":"https://github.com/argoproj/argo-workflows/blob/35bff19146f5a6ada77468c431f2624bd577e373/util/wait/backoff.go#L2-L26","documentation":"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.","triggerScenarios":"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>'.","commonSituations":"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.","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"],"exampleFix":"// before: losing the cause\nerr := wait.ExponentialBackoff(b, f) // only \"timed out waiting for the condition\"\n// after: argo's wrapper keeps both\nerr := waitutil.Backoff(b, f) // \"timed out waiting for the condition: configmaps \\\"x\\\" not found\"\nvar nf *apierr.StatusError\nif errors.As(err, &nf) && apierr.IsNotFound(nf) { /* handle missing resource */ }","handlingStrategy":"try-catch","validationCode":"// preflight the resource the backoff waits on\n_, err := kube.CoreV1().ConfigMaps(ns).Get(ctx, name, metav1.GetOptions{})\nif apierr.IsNotFound(err) { /* create it or fail fast before retrying */ }","typeGuard":null,"tryCatchPattern":"err := waitutil.Backoff(retry.DefaultRetry(ctx), cond)\nif err != nil {\n    var lastErr error\n    if unwrapped := errors.Unwrap(err); unwrapped != nil { lastErr = unwrapped }\n    if errors.Is(err, wait.ErrWaitTimeout) {\n        return fmt.Errorf(\"condition not met after retries; last cause: %v\", lastErr)\n    }\n    return err\n}","preventionTips":["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"],"tags":["retry","backoff","timeout","errors-wrapping"],"backgroundTag":"backoff-retries-exhausted","analyzedSha":"35bff19146f5a6ada77468c431f2624bd577e373","analyzedAt":"2026-09-03T19:34:35.908Z","contentChangedAt":"2026-09-03T19:34:35.908Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}