Handling errors
Decide what to do next from retry.strategy, retry.retryable and billing.status instead of hard-coding a branch per error code.
Every RouteMux error carries the answer to "what should my code do now?" in three structured fields. Read those instead of writing one branch per error code — new codes appear over time, and code written against these fields keeps working.
The short version
async function callWithRouteMuxHandling(send) {
for (let attempt = 0; ; attempt++) {
const res = await send();
if (res.ok) return res;
const { error } = await res.json();
switch (error.retry.strategy) {
case 'BACKOFF': {
if (attempt >= 5) throw new Error(error.code);
// Honour the server's own delay when it sends one.
const base = error.retry.retry_after_seconds ?? Math.min(2 ** attempt, 30);
await sleep((base + Math.random()) * 1000); // add jitter
continue;
}
case 'RETRY':
if (error.retry.retryable && attempt < 3) { await sleep(500); continue; }
throw new Error(error.code);
case 'TOP_UP': return promptUserToAddCredit(error);
case 'SWITCH_MODEL': return retryWithAnotherModel(error);
case 'AUTHENTICATE': return fixApiKey(error);
case 'FIX_REQUEST': throw new Error(error.message); // retrying changes nothing
case 'CONTACT_SUPPORT':
throw new Error(`${error.reference} · ${error.request_id}`);
case 'NONE':
return null; // nothing failed that you need to act on
}
}
}The two retry fields answer different questions
| Field | Question it answers |
|---|---|
retry.retryable | If I send the exact same request again, could it succeed? |
retry.strategy | To actually succeed, what should I change? |
They are independent, and the interesting cases are where they disagree:
| Error | retryable | strategy | Why both are correct |
|---|---|---|---|
MODEL_UNAVAILABLE | true | SWITCH_MODEL | The model may come back, so retrying is not pointless — but switching models is faster. |
JOB_ARTIFACT_NOT_FOUND | false | RETRY | That artifact is gone for good; re-running a new job is what makes sense. |
PLATFORM_INTERNAL_ERROR | true | CONTACT_SUPPORT | Worth one retry, but if it repeats it is our bug, not yours. |
What each strategy means
retry.strategy | Meaning | Do this | Example codes |
|---|---|---|---|
FIX_REQUEST | The request itself is not acceptable | Correct the request. Retrying is pointless | REQUEST_INVALID, MODEL_VISION_UNSUPPORTED |
AUTHENTICATE | Credential problem | Fix or replace the API key; do not retry | AUTH_INVALID_API_KEY, AUTH_API_KEY_EXPIRED |
BACKOFF | Temporary congestion | Wait, then retry. Honour Retry-After | MODEL_BUSY, LIMIT_RATE_EXCEEDED |
RETRY | Transient failure | Retry; no mandatory delay | REQUEST_TIMEOUT, JOB_FAILED |
SWITCH_MODEL | This model cannot serve the request | Use another model | MODEL_NOT_FOUND, MODEL_ACCESS_DENIED |
TOP_UP | Not enough credit | Add credit, then resend | BILLING_INSUFFICIENT_CREDITS |
CONTACT_SUPPORT | You cannot fix this from your side | Open a ticket with reference and request_id | AUTH_ACCOUNT_SUSPENDED, PLATFORM_PRICING_INCOMPLETE |
NONE | Nothing to act on | Continue | JOB_NOT_CANCELLABLE |
Backing off correctly
When an error defines a delay, RouteMux sends it in both places — use whichever your client reads more easily:
- the
Retry-Afterresponse header, in seconds error.retry.retry_after_secondsin the body
MODEL_BUSY sends 5, for example. When no delay is given, use exponential backoff and
add jitter — without it, every client that failed at the same moment retries at the same
moment and rebuilds the spike you are backing off from.
Were you charged?
error.billing.status tells you whether the failed request cost anything.
| Value | Meaning | Safe to retry? |
|---|---|---|
NOT_BILLED | Nothing was charged | Yes, no double-charge risk |
BILLED | The request consumed credit before failing | Retrying costs again — decide deliberately |
Today every error in the catalog is NOT_BILLED: a request rejected with one of these
codes never costs credit, so retrying per strategy cannot double-charge you. BILLED
appears only on request-log entries where usage was already settled before the failure.
Treat the request log as authoritative for money questions. A stream that fails after tokens were produced is the one case where the response body and the final billed amount can differ.
Two things not to do
Do not branch on message. It is localized (via Accept-Language) and reworded as the
product changes. Branch on code; show message to humans.
Do not retry FIX_REQUEST errors. Nothing about the request changed, so the outcome
cannot change. Roughly half of the catalog is FIX_REQUEST — a blind "retry 3 times on any
error" wrapper spends its budget on requests that were never going to succeed.
Finding a specific error
Every code and RMX-… reference is listed in the error reference, which is
searchable and links each entry to a permanent anchor you can share with support.