subscribeFormatRun on a failed run: it resolves, it does not throw

subscribeFormatRun resolves with status failed instead of throwing; it only throws when create is refused or the wait times out. How to branch in TypeScript.

5 min readSume
All posts

subscribeFormatRun from @sume-com/sdk resolves for every terminal status, including failed, canceled and skipped. A failed run is a result you asked for, not an exception, so a try/catch around the call will not catch it. The helper throws only when the create call is refused, when a status read fails and is not transient, or when its own timeout elapses.

This page follows Waiting for runs and jobs for @sume-com/sdk@0.2.0, read 2026-10-02, with failure codes from Errors and spend.

What does it throw, and when?

Unlike the generated operations, which resolve with { data, error }, these helpers throw because a poll loop has nowhere to put a non-result.

What the SDK run helpers throw, read 2026-10-02
ErrorWhenCarries
SumeRunTimeoutErrorThe timeout elapsed firstrunId and lastStatus
SumeRunRequestErrorThe create was refused, or a status read failed and was not transientrunId (or "(not created)") plus the SumeApiError fields
The signal's reasonYou aborted the waitWhatever you passed to abort()

How do I branch on the result?

Read status and error off the receipt exactly as a webhook handler would. completed carries the output, failed carries error (with codes such as unattended_blocked whose message is written to be shown), and canceled and skipped need no output. skipped means a run was already in flight and you passed on_active_run: "skip"; Format runs allow concurrency by default.

import { createSumeClient, subscribeFormatRun } from "@sume-com/sdk";

const client = createSumeClient({ apiKey: process.env.SUME_API_KEY! });

const run = await subscribeFormatRun({
  client,
  path: { handle: "acme", slug: "product-promo" },
  body: { input: { product_url: "https://example.com/p" } },
});

switch (run.status) {
  case "completed":
    console.log(run.primary_output_url);
    break;
  case "failed":
    console.error(run.error?.code, run.error?.message);
    break;
  default:
    console.log("no output:", run.status);
}

Why is a create refusal a throw?

There is no run to wait for. A 403 workspace_key_required on a team Format called with a personal key, a 402 insufficient_credits, or a 400 invalid_request all mean nothing ran and nothing was charged. The helper throws SumeRunRequestError, which extends SumeApiError, so the envelope is available as typed fields: status, code, requestId, retryable, retryAfterSeconds, nextAction and details.

The run helpers always throw SumeRunRequestError itself, never the subclasses such as SumeInsufficientCreditsError, so branch on its status or code rather than instanceof against a subclass.

What should I do after a timeout?

SumeRunTimeoutError does not mean the run failed. The run is still going and still spending, and nothing was lost: store error.runId and error.lastStatus, then read the run later from its result_url. The default timeout for subscribeFormatRun is 20 minutes, longer than waitForRun's 10, because video Formats routinely run 10 to 20 minutes.

For a failed run, remember what the error page says: retry with a new Idempotency-Key, since the old one is bound to the receipt you already hold, and prefer continuing the run with previous_run_id when it left clips behind so finished work is not regenerated.

Common mistakes

Most bugs here come from treating the helper like a promise that rejects on any bad outcome.

  • Wrapping the call in try/catch and assuming the catch covers failed runs.
  • Reading output without checking status and output_error first.
  • Reusing the same Idempotency-Key to retry a failed run, which replays the old receipt.
  • Treating SumeRunTimeoutError as a failure and starting a second paid run.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume