Behio Storefront SDK
Advanced

Error Handling

Handle API errors, retries, and rate limits

Result errors and availability exceptions

Public SDK calls normally return SdkResult<T>. Handle error before using data; do not replace a failed response with an empty array or a 404.

const {data, error} = await storefront.catalog.getProduct("my-product");
if (error?.status === 404) return renderNotFound();
if (error) return renderRetryableError(errorMessage(error, "en"));
return renderProduct(data);

Server catalog clients may enable throwOnAvailabilityError: true. In that mode, exhausted 429/5xx/network/timeout failures throw BehioApiError or BehioNetworkError; business errors such as 400/401/404 still return in the result. A route that renders its own failure UI must handle both paths:

import {
  BehioApiError, BehioNetworkError, toSdkError, type SdkResult,
} from "@behio/storefront-sdk";

async function readCatalogResult<T>(request: Promise<SdkResult<T>>): Promise<SdkResult<T>> {
  try {
    return await request;
  } catch (error) {
    if (error instanceof BehioApiError || error instanceof BehioNetworkError) {
      return {data: null, error: toSdkError(error)};
    }
    throw error;
  }
}

Use this adapter only where the caller renders an explicit error state. Keep the default throwing behavior for other server catalog consumers. Render localized failure content and retry in the first HTML; verify the actual HTML with JavaScript disabled. A client error boundary alone does not prove that the initial response contains the failure message.

If price entitlement lookup fails, the public catalog returns HTTP 503 with be.storefront.pricingUnavailable. It must never substitute unrestricted guest prices. Do not publish a Product/Offer or an indexable error page. An independent review-list failure can keep the product visible while showing an explicit review error and retry; it must not claim that no reviews exist.

Localized Error Messages (SDK 1.10+)

Every public API error now carries a stable machine code in its body (code, e.g. be.storefront.insufficientStock) next to the legacy English message. The SDK ships Czech, Slovak and English translations for all of them, so you can show the customer a proper sentence without maintaining your own error map:

import { errorMessage } from "@behio/storefront-sdk";

const { data, error } = await storefront.cart.addItem({ productId, quantity });
if (error) {
  showToast(errorMessage(error, "cs"));
  // "Produkt Tricko neni v pozadovanem mnozstvi skladem."
}

errorMessage(error, locale) accepts anything an SDK call produces (SdkError, BehioApiError, or a caught unknown) and resolves in this order: translation for the body code (with {params} interpolation), then the English message from the response, then a generic sentence in the requested locale. Supported locales: "cs" | "sk" | "en".

Related helpers, all exported from the package root:

HelperReturns
errorCode(error)the stable be.* code, or null
errorParams(error)interpolation params ({name: "Tricko"})
validationFieldMessages(error, locale)per-field translated map for be.validation.failed bodies ({"email": "Neplatna e-mailova adresa."})

The English message field stays in responses for backward compatibility and will only be removed in a future major version. New code should branch on code (or errorCode()), never on the message text.

Error Properties

BehioApiError

PropertyTypeDescription
statusnumberHTTP status code (400, 401, 404, 429, 500, ...)
codeBehioErrorCodeSemantic error code resolved from status and body
messagestringHuman-readable error message
bodyunknownRaw response body from the API
isRetryablebooleantrue for status >= 500 or 429

BehioNetworkError

PropertyTypeDescription
code"NETWORK_ERROR" | "TIMEOUT"Whether it was a timeout or general network failure
messagestringError description
isRetryablebooleanAlways true

All Error Codes

type BehioErrorCode =
  | "UNAUTHORIZED"          // 401
  | "FORBIDDEN"             // 403
  | "NOT_FOUND"             // 404
  | "VALIDATION_ERROR"      // 400
  | "CONFLICT"              // 409
  | "RATE_LIMITED"          // 429
  | "CART_EMPTY"            // empty cart at checkout
  | "PRODUCT_NOT_FOUND"     // product does not exist
  | "INVALID_CREDENTIALS"   // wrong email/password
  | "INVALID_DISCOUNT"      // discount code invalid
  | "DISCOUNT_EXPIRED"      // discount code expired
  | "TOKEN_EXPIRED"         // auth token expired
  | "TOKEN_INVALID"         // auth token invalid
  | "EMAIL_ALREADY_EXISTS"  // 409 on registration
  | "ORDER_NOT_CANCELLABLE" // order cannot be cancelled
  | "INTERNAL_ERROR"        // 5xx
  | "NETWORK_ERROR"         // network failure
  | "TIMEOUT"               // request timed out
  | "UNKNOWN";              // unrecognized error

Common Error Scenarios

400: Validation Error

The API rejected invalid input (missing fields, wrong format).

const {error} = await storefront.checkout.createOrder(input);
if (error?.status === 400) {
  showFieldErrors(validationFieldMessages(error, "en"));
}

401: Unauthorized

The access token is missing or expired. The SDK handles this automatically (see Token Refresh below), but if refresh also fails, the request returns SdkResult.error.

404: Not Found

The requested resource does not exist.

const {error} = await storefront.catalog.getProduct("nonexistent-slug");
if (error?.status === 404) {
  // Render the missing-resource page. Other errors require their own state.
}

A 404 with the code be.storefront.siteKeyNotAllowed is different: the key belongs to a website that does not sell yet. Shop info, SEO, scripts, pages, menus, blogs, forms, collections and analytics work, while products, cart, checkout and customer accounts stay closed until the merchant adds a shop to the website (Behio admin, or the MCP tool site-commerce-enable). Hide the shop parts of the template instead of showing an error page:

import {errorCode} from "@behio/storefront-sdk";

const {error} = await storefront.catalog.getProducts({limit: 12});
if (errorCode(error) === "be.storefront.siteKeyNotAllowed") {
  // Website without a shop: render the site without the catalog.
}

429: Rate Limited

Too many requests. The SDK will automatically retry (see below).

500: Server Error

Internal server error. The SDK will automatically retry on 5xx errors.

Automatic Retry

The SDK retries failed requests on 5xx errors, 429 (rate limited), and network failures. Configure retry behavior in the constructor:

import { BehioStorefront } from "@behio/storefront-sdk";

const storefront = new BehioStorefront({
  apiKey: "pk_live_xxx",
  retries: 2,       // Number of retries (default: 1)
  retryDelay: 1000,  // Base delay in ms (default: 1000)
  timeout: 30000,    // Request timeout in ms (default: 30000)
});

How Retry Works

  1. The SDK attempts the request.
  2. If it fails with a retryable error, it waits retryDelay * attempt ms (linear backoff).
  3. It retries up to retries times.
  4. If all attempts fail, return SdkResult.error, or throw an availability exception when configured.

For example, with retries: 2 and retryDelay: 1000:

  • Attempt 1 fails → wait 1000ms
  • Attempt 2 fails → wait 2000ms
  • Attempt 3 fails → return the error (or throw with the availability flag)

Set retries: 0 to disable automatic retries entirely.

Availability Errors That Throw (throwOnAvailabilityError)

By default every failure ends up in SdkResult.error and it is easy to write result.data ?? [], which turns a rate limit or an outage into a page that looks like a shop with no products. For server-rendered catalog clients, set:

const storefront = new BehioStorefront({
  apiKey: "pk_live_xxx",
  throwOnAvailabilityError: true,
});

With the flag on, availability failures (429, 5xx, network errors and timeouts, once retries are exhausted) are thrown as BehioApiError / BehioNetworkError instead of being returned in the tuple. Business outcomes (400 validation, 401, 404 not found, 409, ...) still come back as SdkResult.error, so notFound() handling and form validation keep working unchanged. During ISR revalidation a thrown error keeps the last good page; on a fresh render the visitor gets your error boundary instead of a lying empty catalog. Available since 1.8.0.

Token Refresh

When a request returns 401 Unauthorized and the SDK has a refresh token stored, it automatically:

  1. Pauses the failed request
  2. Calls auth.refresh() to get a new access token
  3. Retries the original request with the new token
  4. Emits auth:token-refresh on success

If the refresh itself fails:

  • The SDK clears all tokens
  • Emits auth:token-refresh-failed
  • Returns a result error with status 401 to the original public request
// Listen for refresh failures to redirect to login
storefront.on("auth:token-refresh-failed", () => {
  // Tokens have been cleared automatically
  router.push("/login");
});

storefront.on("auth:token-refresh", () => {
  // Optional: persist the new tokens
  const accessToken = storefront.getAccessToken();
  const refreshToken = storefront.getRefreshToken();
  localStorage.setItem("tokens", JSON.stringify({ accessToken, refreshToken }));
});

Concurrent Requests

If multiple requests receive 401 simultaneously, only one refresh is performed. All other requests wait for the same refresh to complete, then retry with the new token.

Rate Limiting

Monitoring Rate Limits

Check current rate limit status from the latest response headers:

const info = storefront.getRateLimitInfo();
console.log(info.remaining); // Requests remaining (null if no response yet)
console.log(info.reset);     // Unix timestamp when the limit resets

Rate Limit Warning Event

The SDK emits a rate-limit-warning event when remaining requests drop to 5 or fewer:

storefront.on("rate-limit-warning", (data) => {
  const { remaining, reset } = data as { remaining: number; reset: number };
  console.warn(`Rate limit low: ${remaining} requests remaining`);
  console.warn(`Resets at: ${new Date(reset * 1000).toISOString()}`);
});

React Hook Errors

All React hooks are built on TanStack Query. Errors are available through the error property returned by each hook:

import { useProduct } from "@behio/storefront-sdk/react";
import { BehioApiError } from "@behio/storefront-sdk";

function ProductPage({ slug }: { slug: string }) {
  const { data, error, isLoading } = useProduct(slug);

  if (isLoading) return <div>Loading...</div>;

  if (error) {
    if (error instanceof BehioApiError && error.is("NOT_FOUND")) {
      return <div>Product not found</div>;
    }
    return <div>Something went wrong: {error.message}</div>;
  }

  return <h1>{data.name}</h1>;
}

Mutation Errors

For mutations (cart, checkout, auth), errors appear in the error property or can be caught in onError callbacks:

import { useCart } from "@behio/storefront-sdk/react";
import { BehioApiError } from "@behio/storefront-sdk";

function AddToCartButton({ productId }: { productId: string }) {
  const { addItem } = useCart();

  const handleAdd = async () => {
    try {
      await addItem.mutateAsync({ productId, quantity: 1 });
    } catch (error) {
      if (error instanceof BehioApiError) {
        if (error.is("PRODUCT_NOT_FOUND")) {
          showToast("This product is no longer available");
        } else if (error.is("VALIDATION_ERROR")) {
          showToast("Invalid quantity");
        }
      }
    }
  };

  return <button onClick={handleAdd}>Add to Cart</button>;
}

Best Practices

Global Error Listener

Use the error event for centralized logging:

storefront.on("error", (error) => {
  // Send to error tracking (Sentry, etc.)
  console.error("[Behio SDK]", error);
});

React Error Boundary

Wrap your app in an error boundary to catch unhandled errors:

import { QueryErrorResetBoundary } from "@tanstack/react-query";
import { ErrorBoundary } from "react-error-boundary";

function App() {
  return (
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundary
          onReset={reset}
          fallbackRender={({ error, resetErrorBoundary }) => (
            <div>
              <p>Something went wrong: {error.message}</p>
              <button onClick={resetErrorBoundary}>Try again</button>
            </div>
          )}
        >
          <ShopContent />
        </ErrorBoundary>
      )}
    </QueryErrorResetBoundary>
  );
}

Toast Notifications

Map error codes to user-friendly messages:

import { BehioApiError, type BehioErrorCode } from "@behio/storefront-sdk";

const errorMessages: Partial<Record<BehioErrorCode, string>> = {
  UNAUTHORIZED: "Please log in to continue",
  NOT_FOUND: "The requested item was not found",
  VALIDATION_ERROR: "Please check your input",
  RATE_LIMITED: "Too many requests, please wait",
  CART_EMPTY: "Your cart is empty",
  INVALID_CREDENTIALS: "Wrong email or password",
  INVALID_DISCOUNT: "This discount code is invalid",
  DISCOUNT_EXPIRED: "This discount code has expired",
  EMAIL_ALREADY_EXISTS: "An account with this email already exists",
  INTERNAL_ERROR: "Server error, please try again later",
};

function getErrorMessage(error: unknown): string {
  if (error instanceof BehioApiError) {
    return errorMessages[error.code] ?? error.message;
  }
  if (error instanceof Error) {
    return error.message;
  }
  return "An unexpected error occurred";
}

On this page