instanceof SumeNotFoundError is false on a waitForRun 404
waitForRun and subscribeFormatRun throw SumeRunRequestError, not the 401-to-5xx subclasses. Branch on status or code instead; a working catch block.

If error instanceof SumeNotFoundError is false after waitForRun hit a 404, the code is behaving as documented. The run helpers always throw SumeRunRequestError itself, never one of the status subclasses such as SumeNotFoundError or SumeRateLimitError. SumeRunRequestError does extend SumeApiError, so instanceof SumeApiError is true, and the HTTP status and the error code are available as error.status and error.code. Branch on those.
Which class do I get, and where?
The SDK has two ways a failure reaches you. Generated operations such as listFormats resolve with { data, error } and do not throw. The wait helpers throw, because a poll loop has nowhere to put a non-result. Of the two, only the helpers throw SumeRunRequestError, and that class is the one you catch around waitForRun and subscribeFormatRun.
| HTTP status | Subclass of SumeApiError | Thrown by waitForRun? |
|---|---|---|
| 401 | SumeAuthenticationError | No, SumeRunRequestError |
| 402 | SumeInsufficientCreditsError | No, SumeRunRequestError |
| 403 | SumePermissionError | No, SumeRunRequestError |
| 404 | SumeNotFoundError | No, SumeRunRequestError |
| 409 | SumeConflictError | No, SumeRunRequestError |
| 429 | SumeRateLimitError | No, SumeRunRequestError |
| 5xx | SumeServerError | No, SumeRunRequestError |
What does SumeRunRequestError carry?
It carries everything SumeApiError does: code, requestId, retryable, retryAfterSeconds, nextAction, details and the raw body. It adds runId, which is the string "(not created)" when the create call itself was refused. That last case matters for subscribeFormatRun: a 403 workspace_key_required on a team Format arrives before any run exists, so there is nothing to look up later.
Two other errors can come out of the same call. SumeRunTimeoutError means your timeout elapsed first. The run keeps going and keeps billing, so store error.runId and read it back later. And if you pass a signal and abort it, you get the signal's own reason.
A catch block that works
Order the checks from the most specific class to the general one, and branch on status or code inside the SumeRunRequestError branch.
import {
SumeRunRequestError,
SumeRunTimeoutError,
waitForRun,
} from "@sume-com/sdk";
export async function readRun(client, runId) {
try {
return await waitForRun(runId, { client, family: "format" });
} catch (error) {
if (error instanceof SumeRunTimeoutError) {
return { pending: true, runId: error.runId, last: error.lastStatus };
}
if (error instanceof SumeRunRequestError) {
if (error.status === 404) return { missing: true, runId };
if (error.status === 402) return { topUp: true, requestId: error.requestId };
console.error(error.code, error.requestId, error.retryable);
}
throw error;
}
}Why is a 404 on a run id not always a typo?
A 404 format_run_not_found covers an unknown run id and a run that belongs to another owner. The API deliberately answers both the same way, so a run you cannot see reads exactly like one that does not exist. Check the id and check that the key is from the workspace that created the run before you assume the run is gone.
If you do want the subclass types, the SDK exports toSumeApiError(status, body, fallbackMessage). Hand it the status and the error body from a generated operation's { error } result and it returns the matching subclass.
Should I rely on status or on code?
Use the status for the coarse decision and the code for the precise one. The Formats docs say to branch on the HTTP status first and then on code, and that a 4xx at create means nothing ran and nothing was charged, so fix the call instead of retrying it. Retrying a 403 insufficient_scope in a loop is called out as the most common and most expensive mistake.
The message field is written for a human and may change, so log it and never match on it. The code is a stable lowercase token, matching ^[a-z0-9_]+$. The nextAction field is a third option: it names the move, one of authenticate, fix_input, add_funds, retry_later, poll_status, inspect_events or contact_support, which maps neatly onto a switch in your catch block.
Sources
Related posts
More in Developers
- waitForJob throws SumeJobRequestError, not a SumeApiError
A catch for SumeApiError does not see failures from waitForJob. Why SumeJobRequestError has no code or requestId, and how to recover both with toSumeApiError.
- Where the Sume TypeScript SDK runs: Node 18+, Bun, Deno and Workers
@sume-com/sdk has no runtime dependencies and needs only fetch and WebCrypto. Install it, create a client, and know which options and helpers it adds.
- SumeUploadError at step put: the storage upload failed
uploadFile makes three calls. Read SumeUploadError.step to see which failed, and why extra headers on the put step can break the presigned signature.
- Sume video 404 model_not_found: valid model ids for /v1/videos
A video request with an unknown model returns 404 model_not_found, not 400. The ids Sume accepts, how to list them, and the typos that cause it most often.
Written by Sume