LayerHopdocs

Guides

Error handling & retries

Branch on stable error codes, know which errors to retry, and recover from expired quotes and provider outages.

Open in ClaudeOpen in ChatGPT

The error shape#

Every non-2xx response has the same body, and every response has an x-request-id header:

JSON
{
  "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. message is for humans and may change.
  • Log requestId; include it when you report a problem.
  • docsUrl points at the error catalog.

The SDK throws LayerHopApiError with code, status, message and requestId:

TypeScript
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#

TypeScript
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:

  1. Re-quotes on a timer (for example every 15–20 s) while the user is looking at a price.
  2. Builds the transaction only after the user confirms.
  3. On QUOTE_EXPIRED / QUOTE_NOT_FOUND, fetches a fresh quote, shows the new output, and asks again — never silently signs a worse price.
TypeScript
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-Key header on POST requests and LayerHop-side rate limits with Retry-After.