Errors and rate limits
Every storefront error answers one envelope with a stable code, and the SDK turns each into a typed error. This page lists them all, with the rate limits and what the client and the hooks do about them.
The error envelope
{
"error": {
"code": "INVALID_REQUEST",
"message": "The request is not valid.",
"requestId": "8f3c2a9e-1b4d-4f6a-9c2e-7d5b3a1f0e64",
"issues": [{ "path": "limit", "message": "Must be a whole number from 1 to 100." }]
}
}
-
error.codeis stable; branch on it, never on the message.error.requestIdidentifies the request when you ask for help. -
error.issues(at most 20) names the fields at fault, as{ path, message }. -
CART_REJECTEDaddserror.throttleCode, andCART_ITEMS_INVALIDadds a top-levelitemslist of refused lines. -
Throttle's cart-session routes answer their own envelope,
{ error: { code, message, details? } }, with lowercase codes.
The SDK's errors
Everything the SDK throws is a CatalogError (which extends ThrottleError from @usethrottle/errors), with code, message, statusCode (0 when nothing answered), source
('catalog', 'throttle' for Throttle's cart
session, or 'client' when the SDK made it), retryable, retryAfterSeconds, issues, details and requestId.
| Class | When |
|---|---|
RateLimitedError | Every 429. retryAfterSeconds is always set: from Retry-After, or 60 when that is missing or unusable. |
OriginNotAllowedError | ORIGIN_NOT_ALLOWED, SERVER_KEY_IN_BROWSER, KEY_NOT_ALLOWED_IN_URL and Throttle's origin_not_allowed. |
CustomerTokenInvalidError | A refused customer token on a call that cannot fall back to regular prices. |
CartLineError | CART_ITEMS_INVALID and Throttle's quantity_unavailable and line_not_purchasable, with lines: { sku, code, maxQuantity? }[]. |
TimeoutError | TIMEOUT: no answer within timeoutMs. |
NetworkError | NETWORK_ERROR: the request never got an answer (DNS, a CORS refusal, a dropped connection). |
FeatureUnavailableError | FEATURE_UNAVAILABLE: the catalog does not have this route yet (below). |
CatalogError | Everything else. |
import {
CartLineError,
createCatalogClient,
CustomerTokenInvalidError,
FeatureUnavailableError,
isCatalogError,
NetworkError,
OriginNotAllowedError,
RateLimitedError,
TimeoutError,
} from '@usethrottle/catalog';
const catalog = createCatalogClient({
publishableKey: 'pc_pk_your_browser_key',
});
try {
await catalog.products.list({ q: 'brake pads' });
} catch (error) {
if (error instanceof RateLimitedError) {
// Every 429. The client never retries one itself.
console.warn(`Slow down: retry in ${error.retryAfterSeconds} s`);
} else if (error instanceof TimeoutError || error instanceof NetworkError) {
console.warn('No answer; safe to try again.');
} else if (error instanceof OriginNotAllowedError) {
console.error('This website is not on the key’s allowed websites.');
} else if (error instanceof FeatureUnavailableError) {
console.error('This catalog does not have that route yet.');
} else if (error instanceof CustomerTokenInvalidError || error instanceof CartLineError) {
console.error(error.code);
} else if (isCatalogError(error)) {
// Everything else: branch on the code. source says who answered.
console.error(error.code, error.statusCode, error.source, error.requestId, error.issues);
} else {
// Your own AbortSignal's reason, or a bug: not a catalog error.
throw error;
}
}
- FeatureUnavailableError. A
404that carries the headerX-Catalog-Route: unmatchedmeans the catalog has no such route: an older catalog than the SDK. A404from a route that never answers a missing resource (config, the category tree,tags, the collections list,suggest,carts/checkandcarts) means the same. Any other404isNOT_FOUND. - Your own abort rethrows your
AbortSignal's reason, never wrapped in aCatalogError. -
isCatalogErroralso recognises an error from another copy of the SDK, such as the CommonJS build in an app that loads both;instanceofon a class holds within one build only.
Codes the SDK makes
| Code | Meaning |
|---|---|
TIMEOUT | No answer within timeoutMs. Retryable. |
NETWORK_ERROR | The request failed before any answer. Retryable. |
FEATURE_UNAVAILABLE | The catalog has no such route (see above). |
INVALID_RESPONSE | An answer that is not the JSON expected, such as an HTML error page from a proxy. Its text is never put in the message. |
INVALID_PUBLISHABLE_KEY | Thrown by createCatalogClient for a key that is not pc_pk_…. The key is never in the message. |
CLIENT_ERROR | A cartStorage adapter (or another of your functions) threw inside a hook action. |
Timeouts. timeoutMs (10 seconds by default, per client or per
call) covers one request. Reading the customer token gets its own full timeoutMs
first, so a call that waits on a slow token refresh can take up to about twice as long.
Every storefront error code
Every code a /v1/storefront route answers, from the OpenAPI document, with the class the
SDK throws for it. Retryable means the same request may succeed later.
| error.code | HTTP | Retryable | SDK class | Meaning |
|---|---|---|---|---|
INVALID_REQUEST | 400 | No | CatalogError | A parameter or body field is not valid; issues names each one. Also the SDK’s own refusal of a bad option or URL (statusCode 0, source client). |
AUTHENTICATION_FAILED | 401 | No | CatalogError | The publishable key is missing, unknown, revoked or a secret key. |
INFRASTRUCTURE_UNAVAILABLE | 503 | Yes | CatalogError | A catalog service the request needs is down for a moment. |
PROVIDER_UNAVAILABLE | 503 | Yes | CatalogError | An outside service the request needs did not answer. |
INTERNAL_ERROR | 500 | No | CatalogError | An unexpected error. Quote the requestId. |
CUSTOMER_TOKEN_INVALID | 401 | No | CustomerTokenInvalidError | The customer token is expired, revoked or for another store, or missing where one is required. |
ORIGIN_NOT_ALLOWED | 403 | No | OriginNotAllowedError | The request’s Origin is not on the key’s allowed websites, or a browser key was used without an Origin. |
SERVER_KEY_IN_BROWSER | 403 | No | OriginNotAllowedError | A server key was used on a request that carries an Origin. |
KEY_NOT_ALLOWED_IN_URL | 403 | No | OriginNotAllowedError | A server key was sent in the URL (?key=). Server keys go only in X-Catalog-Key. |
NOT_FOUND | 404 | No | CatalogError | No public product, category, collection or route with that handle (a draft, archived or unknown handle answers the same). |
RATE_LIMITED | 429 | Yes | RateLimitedError | Too many requests. Retry-After says when to try again. |
STOREFRONT_UNAVAILABLE | 503 | Yes | CatalogError | The catalog could not be reached. Never cached. |
REQUEST_BODY_TOO_LARGE | 413 | No | CatalogError | The request body is over its limit (64 KB for carts and reviews). |
CART_CURRENCY_REQUIRED | 422 | No | CatalogError | A cart with no currency, and no default currency in the store’s settings. |
CART_CURRENCY_NOT_ENABLED | 422 | No | CatalogError | Throttle has not enabled that currency for the store’s carts. |
CHECKOUT_NOT_CONFIGURED | 409 | No | CatalogError | The catalog has no Throttle credentials for the store yet. |
EXTENSION_UPGRADE_REQUIRED | 409 | No | CatalogError | The store has not accepted the Product Catalog extension version this feature needs. |
CART_ITEMS_INVALID | 422 | No | CartLineError | Some cart lines cannot be bought; items holds a code (and maxQuantity) per line. |
CART_REJECTED | 502 | No | CatalogError | Throttle refused to create the cart; error.throttleCode is its reason. |
CART_UNAVAILABLE | 503 | Yes | CatalogError | Throttle could not be reached to create the cart. |
CHECKOUT_UNAVAILABLE | 503 | Yes | CatalogError | Legacy checkout route: Throttle could not be reached. |
RETURN_URL_NOT_ALLOWED | 422 | No | CatalogError | A return URL is not on Checkout settings › Allowed return websites; one issue per field. |
CHECKOUT_TOTAL_TOO_LARGE | 422 | No | CatalogError | Legacy checkout route: the cart total is too large. |
CHECKOUT_CART_MISMATCH | 409 | No | CatalogError | Legacy checkout route: the Throttle cart did not match the request. Start a new checkout. |
CHECKOUT_ITEMS_INVALID | 422 | No | CatalogError | Legacy checkout route: some lines cannot be bought. |
CHECKOUT_REJECTED | 502 | No | CatalogError | Legacy checkout route: Throttle refused the checkout. |
CURRENCY_NOT_ENABLED | 400 | No | CatalogError | The currency parameter names a currency the store does not sell in. |
STOREFRONT_INDEXING | 503 | Yes | CatalogError | The catalog is building its first storefront index. Retry-After: 30. |
EMAIL_NOT_VERIFIED | 403 | No | CatalogError | Reviews: the shopper has not verified their email address. |
NOT_VERIFIED_BUYER | 403 | No | CatalogError | Reviews: only verified buyers may review, and the shopper has no paid order for the product. |
PURCHASE_REFUNDED | 403 | No | CatalogError | Reviews: the order behind the purchase no longer counts (refunded or cancelled). |
ALREADY_REVIEWED | 409 | No | CatalogError | Reviews: one review per customer and product. |
REVIEW_QUEUE_FULL | 429 | Yes | RateLimitedError | Reviews: 500 reviews already wait for approval. Retry-After: 900. |
REVIEW_VERIFICATION_UNAVAILABLE | 503 | Yes | CatalogError | Reviews: the purchase could not be confirmed with Throttle. Nothing was saved. |
Throttle's cart-session codes (quantity_unavailable, cart_prices_changed, cart_session_expired and
the others) reach you with source: 'throttle'; they are listed on Cart and checkout.
cart_session_expired passes details.storefrontUrl through as it was stored, unvalidated. Never render it as a
link or navigate to it without your own check: http or https only, on an origin you trust.
Rate limits
| Key type | Per visitor (IP) | Per key, all visitors |
|---|---|---|
| Browser, app and legacy | 300 requests a minute; 600 for suggestions | 12,000 requests a minute |
| Server | None | 60,000 requests a minute |
- Review submission adds 5 attempts a minute per customer and 10 accepted reviews an hour per IP address (server keys skip the IP cap).
-
Over a limit the answer is
429 RATE_LIMITEDwithRetry-Afterin seconds. Wait that long; the SDK exposes it asRateLimitedError.retryAfterSeconds. - The client never retries a
429itself, so a burst never turns into a retry storm. - The per-visitor count is kept per Cloudflare location and is approximate: a fast burst can pass somewhat more than the limit before it is refused. The per-key limit still caps a key.
In the hooks. A catalog 429 RATE_LIMITED that a read hook or
a useCart change meets pauses every read of its CatalogProvider until retryAfterSeconds:
- The hook that met it keeps its data and shows the
RateLimitedError. -
A hook mounted during the pause makes no request. With nothing cached for its query, it shows a
RateLimitedErrorwith the seconds left, not a loading state; with a cached answer, it shows that. -
useCartanduseSubmitReviewactions reject at once with that error, with no request. - When the pause ends, each mounted query fetches once.
-
Every other
429goes to the caller only and pauses nothing: a429from Throttle's cart session, and every429from a review submit (RATE_LIMITEDfrom the per-customer limit or the per-IP review cap, andREVIEW_QUEUE_FULL).
While the catalog is indexing
When a store first turns on the storefront, the catalog builds its storefront index. Until it is done, plain
product browsing answers 200 with indexing: true
(no filters, and a null total), and requests that need the index answer 503 STOREFRONT_INDEXING with Retry-After: 30.
See the first
backfill.
- The client retries a read once after
Retry-Afterwhen that is 30 seconds or less (indexingRetry: { maxWaitSeconds }changes it,falseturns it off); otherwise it throws withretryAfterSeconds. - The hooks never wait inside a call. They show the
503and fetch once more atRetry-After; a list answered withindexing: trueis fetched once more after 30 seconds.