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.

A catch that tests error instanceof SumeApiError will not match a failed status read inside waitForJob. That helper throws SumeJobRequestError, which extends plain Error and carries only jobId, status and body. There is no code, requestId, retryable or nextAction on it. waitForRun is different: its SumeRunRequestError does extend SumeApiError. The two siblings look alike and are not.
How do the two wait helpers differ?
Runs and jobs are separate surfaces, so the helpers grew up separately. The table lists what the SDK source and the docs say about each.
| waitForRun | waitForJob | |
|---|---|---|
| Reads | Format, Action or Agent runs | Generation jobs at /v1/jobs/:id |
| Request error class | SumeRunRequestError (extends SumeApiError) | SumeJobRequestError (extends Error) |
| Typed fields | code, requestId, retryable, nextAction, details | jobId, status, body |
| Timeout default | 10 minutes | 20 minutes |
| Timeout error | SumeRunTimeoutError | SumeJobTimeoutError |
| Tolerates failed status reads | Yes, 6 in a row by default | No, the first failed read throws |
Will a single 429 end my waitForJob?
Not usually. createSumeClient retries 408, 429 and 5xx on reads, twice by default with exponential backoff and jitter, and it honours retry-after. Only after those retries are spent does the status read fail and waitForJob throw. So a short rate-limit blip is absorbed by the client. A longer one reaches you as SumeJobRequestError with status 429, and the job is still running and still billing.
That is the real hazard of the type mismatch. If your handler treats an unmatched error as fatal and marks the job failed, you have lost track of a live paid job. Keep the job id and read it again.
Recover the typed fields
SumeJobRequestError.body holds the parsed error envelope when the API sent one. Pass it with the status to toSumeApiError and you get the same typed class waitForRun would have thrown.
import {
SumeJobRequestError,
SumeJobTimeoutError,
toSumeApiError,
waitForJob,
} from "@sume-com/sdk";
export async function finishJob(client, jobId) {
try {
return await waitForJob(jobId, { client });
} catch (error) {
if (error instanceof SumeJobTimeoutError) {
return { stillRunning: true, jobId: error.jobId };
}
if (error instanceof SumeJobRequestError) {
const typed = toSumeApiError(error.status, error.body, error.message);
console.error(typed.code, typed.requestId, typed.retryAfterSeconds);
if (typed.retryable) return { retryLater: true, jobId };
}
throw error;
}
}What does the resolved value tell me?
waitForJob resolves for every terminal status: completed, failed and canceled. It reads the final record from /v1/jobs/:id rather than /result, because /result answers 409 job_not_completed for failed and canceled jobs. So check job.status on the value you get back, and read job.error for a failure. A timeout never cancels the job; read it later with its id or cancel it deliberately.
Why is the job error separate at all?
The error classes were typed for the run surface first: SumeRunRequestError was made a SumeApiError subclass so that .code, .requestId, .retryable and .retryAfterSeconds come without unwrapping .body by hand, and .body and .status kept their old meaning for callers written against the previous shape. The job helper still has the earlier shape. If you wrote shared error-handling code for both families, test it against a failing waitForJob call too, for example by waiting on a job id that does not exist, and confirm which branch it takes.
Until the shapes are unified, a small adapter at your boundary keeps the rest of your code uniform: convert any SumeJobRequestError with toSumeApiError as above, and let everything downstream see only SumeApiError.
How do I keep the job id safe?
Persist the id the moment you submit, before you start waiting. The submit response gives it as request_id inside the job envelope, and the same id works with getApiJob and cancelApiJob. If your process dies mid-wait, the job is still running and still billing, so the stored id is what lets the next process pick it up instead of submitting a second paid job.
Sources
Related posts
More in Developers
- 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.
- Sume video callback_url must be HTTPS: a webhook instead of polling
POST /v1/videos accepts callback_url, which must be HTTPS. Event names, the signature header, retries, and when a poll loop is still the safer choice.
Written by Sume