Guides
Error handling & retries
Branch on stable error codes, know which errors to retry, and recover from expired quotes and provider outages.
The error shape#
Every non-2xx response has the same body, and every response has an x-request-id header:
{
"error": {
"code": "QUOTE_EXPIRED",
"message": "Quote expired, request a new one",
"requestId": "0d5e1f6a-3b2c-4d1e-8f9a-7b6c5d4e3f21",
"docsUrl": "https://docs.layerhop.com/errors/QUOTE_EXPIRED"
}
}- Branch on
code— it is stable.messageis for humans and may change. - Log
requestId; include it when you report a problem. docsUrlpoints at the error catalog.
The SDK throws LayerHopApiError with code, status, message and requestId:
import { LayerHopApiError } from "@layerhop/sdk";
try {
await lh.buildTx(quoteId);
} catch (e) {
if (e instanceof LayerHopApiError && (e.code === "QUOTE_EXPIRED" || e.code === "QUOTE_NOT_FOUND")) {
// re-quote
} else throw e;
}What to retry#
| Code | Retry? | How |
|---|---|---|
INVALID_PARAMS, UNSUPPORTED_CHAIN, UNSUPPORTED_TOKEN, AMOUNT_TOO_SMALL, AMOUNT_TOO_LARGE |
No | Fix the request. |
QUOTE_EXPIRED, QUOTE_NOT_FOUND |
After a new quote | Re-quote and ask the user to confirm the new price. |
NO_ROUTE |
With backoff | Providers may be briefly down; also try other amounts/tokens. |
PROVIDER_ERROR, PROVIDER_TIMEOUT, RATE_LIMITED, INTERNAL |
With backoff | Exponential backoff with jitter, a few attempts. |
ORDER_NOT_FOUND |
With backoff | Right after broadcast the provider may not have indexed the order yet. |
UNAUTHORIZED |
No | A provider key problem on the API side; other quotes still work. |
A retry helper#
import { LayerHopApiError } from "@layerhop/sdk";
const RETRYABLE = new Set(["NO_ROUTE", "PROVIDER_ERROR", "PROVIDER_TIMEOUT", "RATE_LIMITED", "INTERNAL", "ORDER_NOT_FOUND"]);
export async function withRetry<T>(fn: () => Promise<T>, attempts = 4, baseMs = 500): Promise<T> {
for (let i = 0; ; i++) {
try {
return await fn();
} catch (e) {
const retryable = e instanceof LayerHopApiError ? RETRYABLE.has(e.code) : e instanceof TypeError; // TypeError: network failure
if (!retryable || i >= attempts - 1) throw e;
await new Promise((r) => setTimeout(r, baseMs * 2 ** i + Math.random() * baseMs));
}
}
}
const quotes = await withRetry(() => lh.quote(params));Expired quotes#
Quotes live 30–60 seconds. A robust UI:
- Re-quotes on a timer (for example every 15–20 s) while the user is looking at a price.
- Builds the transaction only after the user confirms.
- On
QUOTE_EXPIRED/QUOTE_NOT_FOUND, fetches a fresh quote, shows the new output, and asks again — never silently signs a worse price.
async function buildFresh(params: QuoteParams) {
let { best } = await lh.quote(params);
if (best.expiresAt - Date.now() / 1000 < 5) ({ best } = await lh.quote(params));
return { quote: best, tx: await lh.buildTx(best.quoteId) };
}Per-provider errors in a successful quote#
A 200 from GET /v1/quote can still contain errors. Each entry has provider, code and message. Use them for diagnostics ("deBridge: amount too small") — not to block the user, since the quotes you got are valid.
After the user signed#
Once a transaction is on chain, the order belongs to the provider's protocol: errors from the status endpoint mean "could not check right now", not "the swap failed". Keep polling with backoff, and show the source transaction in the explorer.
Coming soon
an
Idempotency-Keyheader on POST requests and LayerHop-side rate limits withRetry-After.