{"record":{"id":"16d2d7d2080c6b47","repo":"heygen-com/hyperframes","slug":"stage-budget-exceeded-while-label-budgetms-m","errorCode":null,"errorMessage":"stage budget exceeded while ${label} (${budgetMs}ms)","messagePattern":"stage budget exceeded while (.+?) \\((.+?)ms\\)","errorType":"exception","errorClass":"StageBudgetTimeoutError","httpStatus":null,"severity":"error","filePath":"packages/cli/src/capture/captureTimeout.ts","lineNumber":63,"sourceCode":"\nexport function isProtocolEvaluateTimeoutError(err: unknown): boolean {\n  if (err instanceof TimeoutError) {\n    return !hasNavigationTimeoutMessage(err.message);\n  }\n  return hasProtocolEvaluateTimeoutMessage(errorMessage(err));\n}\n\nexport function isDegradableEvaluateTimeoutError(err: unknown): boolean {\n  return isStageBudgetTimeoutError(err) || isProtocolEvaluateTimeoutError(err);\n}\n\nexport async function withRemainingBudget<T>(\n  work: Promise<T>,\n  remainingMs: number,\n  label: string,\n): Promise<T> {\n  if (!(remainingMs > 0)) {\n    throw new StageBudgetTimeoutError(label, Math.max(0, remainingMs));\n  }\n\n  let timer: ReturnType<typeof setTimeout> | undefined;\n  try {\n    return await Promise.race([\n      work,\n      new Promise<never>((_resolve, reject) => {\n        timer = setTimeout(() => {\n          reject(new StageBudgetTimeoutError(label, remainingMs));\n        }, remainingMs);\n      }),\n    ]);\n  } finally {\n    if (timer !== undefined) {\n      clearTimeout(timer);\n    }\n  }\n}","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/heygen-com/hyperframes/blob/c2996c8626135db5253519359d8a063d3bafad8d/packages/cli/src/capture/captureTimeout.ts#L45-L81","documentation":"Thrown by withRemainingBudget() when a capture stage exceeded its allotted wall-clock budget. The function races the work promise against a setTimeout(remainingMs); if the timer fires first (or remainingMs was already <= 0 on entry), it rejects with a StageBudgetTimeoutError carrying the label and budgetMs. This is the mechanism that bounds each capture stage so a hung page does not stall the whole render indefinitely.","triggerScenarios":"withRemainingBudget(work, remainingMs, label) is called with a remaining budget that is exhausted either up-front (remainingMs <= 0) or because the work promise did not settle before the setTimeout fired. Typical during in-page script evaluation that hangs (infinite loop, blocked on a resource).","commonSituations":"Composition runs a heavy in-page animation/JS that blocks the main thread past the stage budget; a network resource the page waits on is slow/blocked; the overall capture timeout is too tight for a complex composition; the page hung on a WebGPU/shader compile.","solutions":["Increase the stage/capture timeout if the composition legitimately needs more time (pass a larger --timeout or the relevant option).","Profile the composition's in-page script — a hang usually means an infinite loop or a synchronous wait; optimize or make it async.","Ensure external resources the page depends on are local or fast (deterministic rendering forbids render-time network fetches).","If the budget was already <= 0 on entry, an earlier stage overran — investigate the label of the prior stage."],"exampleFix":"// before — default timeout too tight for heavy comp\n$ hyperframes render heavy.html\n// allow more time\n$ hyperframes render heavy.html --timeout 120000\n// in composition: avoid blocking the main thread\n// before (hangs): while(true){}\n// after: yield with requestAnimationFrame","handlingStrategy":"retry","validationCode":"// Ensure the stage budget is positive before calling withRemainingBudget.\nif (!(remainingMs > 0)) {\n  throw new Error(`No budget left for stage '${label}' — increase the capture timeout.`);\n}","typeGuard":null,"tryCatchPattern":"import { isDegradableEvaluateTimeoutError } from '@hyperframes/cli/capture/captureTimeout';\ntry {\n  await withRemainingBudget(work, remainingMs, label);\n} catch (err) {\n  if (isDegradableEvaluateTimeoutError(err)) {\n    // degrade gracefully: skip optional capture step or lower fidelity\n  } else throw err;\n}","preventionTips":["Size the capture timeout to the composition's complexity; raise --timeout for heavy comps.","Keep in-page scripts non-blocking; yield to the event loop where possible.","Avoid render-time network fetches in compositions (deterministic-rendering rule).","Use isDegradableEvaluateTimeoutError to classify retryable vs fatal capture failures."],"tags":["capture","timeout","budget","performance","runtime"],"backgroundTag":null,"analyzedSha":"c2996c8626135db5253519359d8a063d3bafad8d","analyzedAt":"2026-08-12T22:18:56.877Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}