Skip to content

[copilot-finds] Bug: Retry handler returning undefined/null/NaN/Infinity causes infinite retry loop or invalid timer #140

@github-actions

Description

@github-actions

Problem

In packages/durabletask-js/src/worker/orchestration-executor.ts, the tryHandleRetry method uses a negative check (retryResult !== false) to determine whether a custom retry handler wants to retry a failed task. This means any value that is not strictly false — including undefined, null, NaN, and Infinity — is treated as "retry".

This causes two distinct failure modes:

  1. Infinite retry loop: If a retry handler has a code path that returns undefined (common JavaScript mistake — a missing return statement in a branch), the task is rescheduled immediately and retries forever, without any delay.

  2. Invalid timer creation: If the handler returns NaN or Infinity, these pass the typeof === "number" check but create timers with invalid fire-at dates, causing unpredictable behavior.

Affected code (line ~795):

if (retryResult !== false) {  // BUG: undefined, null, NaN, Infinity all pass this check

Example — handler with missing return:

const retryHandler = async (ctx) => {
  if (ctx.lastAttemptNumber >= 5) {
    return false;
  }
  // Oops — forgot "return true" here, returns undefined
};

This handler intends to stop retrying after 5 attempts, but on attempts 1–4 it returns undefined, which the SDK treats as "retry immediately" — creating an infinite loop until the 5th attempt.

While TypeScript types (RetryHandlerResult = boolean | number) catch this at compile time, JavaScript consumers and as any casts bypass this protection. The SDK should defensively handle unexpected return values at runtime.

Root Cause

The condition retryResult !== false is a negative check that accepts ANY value except false. The correct pattern is a positive check: only accept values that are explicitly true or a finite number.

Proposed Fix

Change the condition to a positive check:

if (retryResult === true || (typeof retryResult === "number" && Number.isFinite(retryResult)))

This ensures:

  • true → retry immediately ✓
  • false → stop retrying ✓
  • Finite positive number → retry with delay ✓
  • Finite zero/negative number → retry immediately ✓ (preserves existing behavior)
  • undefined, null → stop retrying ✓ (FIXED)
  • NaN, Infinity → stop retrying ✓ (FIXED)

Impact

  • Severity: High — can cause infinite retry loops, resource exhaustion, and orchestration hangs
  • Affected scenarios: Any orchestration using AsyncRetryHandler or RetryHandler where the handler has a code path that does not explicitly return true, false, or a number
  • Likelihood: Medium — most common in JavaScript consumers or TypeScript code with partial branch coverage

Metadata

Metadata

Assignees

No one assigned

    Labels

    copilot-findsFindings from daily automated code review agent

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions