Skip to content

feat(async/unstable): add getDelay option to retry() - #7317

Open
tomas-zijdemans wants to merge 1 commit into
denoland:mainfrom
tomas-zijdemans:feat-async-retry-get-delay
Open

feat(async/unstable): add getDelay option to retry()#7317
tomas-zijdemans wants to merge 1 commit into
denoland:mainfrom
tomas-zijdemans:feat-async-retry-get-delay

Conversation

@tomas-zijdemans

Copy link
Copy Markdown
Contributor

Adds getDelay to an unstable retry(), so the caller picks the wait before each retry.

Right now a 429 with Retry-After: 30 cannot be honored. retry() computes the backoff inside the loop and never lets it out, so a caller who knows how long the server wants to be left alone has three bad options: sleep inside fn and get std's backoff added on top, rewrite the loop to change one number, or stop retrying and hand the problem upstairs. Two clients I work with took the first two.

The hook runs after isRetriable accepts the error and the attempt budget is checked, right before the wait:

await retry(request, {
  isRetriable: isRetriableFetchError,
  getDelay(error, _attempt, computedDelay) {
    if (!(error instanceof Response)) return computedDelay;
    const serverDelay = parseRetryAfter(error.headers.get("retry-after"));
    return Math.max(computedDelay, serverDelay ?? 0);
  },
});

Header parsing stays with the caller. @std/async gains no HTTP dependency.

Worth arguing about

  • The returned value is the whole wait: no jitter on top, no minTimeout/maxTimeout clamp. Clamping could retry before the server asked, which is the bug this exists to fix.
  • Bad returns are fatal. TypeError for a non-number, RangeError for a negative number or NaN, no quiet fallback to the computed delay.
  • Infinity is legal and waits until the signal aborts. It does not mean "stop retrying".
  • The new module copies the stable loop, because the hook belongs inside it. It extends the stable RetryOptions and re-exports RetryError, so instanceof holds across both entrypoints. Stable retry() is untouched.

Testing

21 tests on the new entrypoint, with FakeTime and a stubbed Math.random: hook ordering, overrides above the cap and below the base, invalid returns, aborts before, during and inside the hook, zero, fractions, waits past 2 ** 31 - 1, and cancellable Infinity. The new module is at 100% line, branch and function coverage. deno task ok and deno task typos are clean apart from four fs/walk_test.ts failures that reproduce on main in my environment.

Follows #7296. #7297 proposed an observation-only onRetry and is closed. Returning computedDelay after logging covers that case too.

I used Claude Code to help investigate and write this change.

Adds an unstable retry() entrypoint whose getDelay hook selects the wait
before each retry, so a caller can honor Retry-After without owning the
retry loop. The hook receives the thrown value, the 1-based failed attempt
number, and the delay the backoff computed; its return value replaces the
whole wait, without further jitter or clamping.
@github-actions github-actions Bot added the async label Sep 13, 2026
@codecov

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.04%. Comparing base (ca58f94) to head (5a23758).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7317   +/-   ##
=======================================
  Coverage   95.03%   95.04%           
=======================================
  Files         617      618    +1     
  Lines       51637    51723   +86     
  Branches     9359     9388   +29     
=======================================
+ Hits        49075    49160   +85     
  Misses       2021     2021           
- Partials      541      542    +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant