heygen-com/hyperframes · error · PollTimeoutError

Render ${lastDetail.render_id} did not reach a terminal stat

Error message

Render ${lastDetail.render_id} did not reach a terminal state within ${Math.round(elapsedMs / 1000)}s

What it means

PollTimeoutError is thrown by pollUntilTerminal when the render has not reached 'completed' or 'failed' within maxWaitMs (default 60 min, interval 10s). It is a dedicated Error subclass that carries lastDetail (the most recent GET /v3/hyperframes/renders/{id} response) so callers can inspect status, progress, and any partial error info. Underlying request errors (404/401/5xx) bubble immediately and are intentionally NOT retried inside the loop.

Source

Thrown at packages/cli/src/cloud/poll.ts:83

  // instead of waiting out the full interval. Tests inject a no-op
  // sleep that ignores the signal — that's fine, they don't abort.
  const sleep = options.sleep ?? defaultAbortableSleep(options.signal);

  const started = now();

  while (true) {
    if (options.signal?.aborted) {
      throw signalAbortError(options.signal);
    }
    const detail = await client.getRender({ render_id: renderId, signal: options.signal });
    const elapsed = now() - started;
    options.onTick?.(detail, elapsed);

    if (isTerminal(detail.status)) {
      return detail;
    }
    if (elapsed >= maxWaitMs) {
      throw new PollTimeoutError(detail, elapsed);
    }
    await sleep(intervalMs);
  }
}

function signalAbortError(signal: AbortSignal): Error {
  const reason = signal.reason;
  return reason instanceof Error ? reason : new Error("Poll aborted");
}

function defaultAbortableSleep(signal?: AbortSignal): (ms: number) => Promise<void> {
  // fallow-ignore-next-line complexity
  return (ms: number) =>
    new Promise<void>((resolve, reject) => {
      const onAbort = (): void => {
        clearTimeout(timer);
        reject(signalAbortError(signal!));
      };

View on GitHub (pinned to c2996c8626)

Solutions

  1. Catch PollTimeoutError specifically, read err.lastDetail, and decide: resume polling with another pollUntilTerminal call, or surface failure.
  2. If the render is legitimately long, raise maxWaitMs (e.g. 2h) and pass a generous intervalMs.
  3. Check the render id via `hyperframes cloud status <id>` or the dashboard to see if the workflow is actually progressing.
  4. If lastDetail.status is stale across many ticks, report a stuck workflow to the API owner rather than looping forever.

Example fix

// before: single 60-min cap, hard fail on timeout
const detail = await pollUntilTerminal(client, id);

// after: resume across multiple windows, keep lastDetail
let detail;
try {
  detail = await pollUntilTerminal(client, id, { maxWaitMs: 30 * 60_000 });
} catch (err) {
  if (err instanceof PollTimeoutError) {
    detail = err.lastDetail;          // inspect, then resume or report
    detail = await pollUntilTerminal(client, id, { maxWaitMs: 30 * 60_000 });
  } else throw err;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check: confirm the render id exists before polling
const detail = await client.getRender({ render_id: id });
if (!detail) throw new Error(`render ${id} not found`);

Type guard

function isPollTimeoutError(err: unknown): err is PollTimeoutError {
  return err instanceof Error && err.name === 'PollTimeoutError';
}

Try / catch

try {
  detail = await pollUntilTerminal(client, id, { maxWaitMs: 30 * 60_000 });
} catch (err) {
  if (err instanceof PollTimeoutError) {
    detail = err.lastDetail;  // inspect, then resume or escalate
  } else throw err;
}

Prevention

When it happens

Trigger: A render that stays in 'queued'/'processing' longer than maxWaitMs; a Temporal workflow whose start_to_close is longer than the poll cap; the API stalling without flipping to a terminal status; maxWaitMs explicitly lowered by the caller for a fast-fail policy.

Common situations: A genuinely long render (high-resolution, many scenes) exceeding the default cap; a stuck server-side workflow; calling poll with a too-small maxWaitMs; network blips that repeatedly delay each getRender without erroring.

Related errors


AI-assisted analysis of heygen-com/hyperframes@c2996c8626 (2026-08-12). Data as JSON: /api/errors/ec2f5a910b5be1db. Report an issue: GitHub.