Behio Storefront SDK
Advanced

Error Handling

Handle API errors, retries, and rate limits

Error Types

The SDK throws two error classes, both exported from @behio/storefront-sdk:

ClassWhenisRetryable
BehioApiErrorAPI returned a non-2xx responsetrue for 5xx and 429
BehioNetworkErrorNetwork failure or request timeoutalways true
import { BehioApiError, BehioNetworkError } from "@behio/storefront-sdk";

Catching Errors

Use instanceof to distinguish error types:

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

try {
  const product = await storefront.catalog.getProduct("my-product");
} catch (error) {
  if (error instanceof BehioApiError) {
    console.error(`API error ${error.status}: ${error.message}`);
    console.error("Error code:", error.code);
    console.error("Response body:", error.body);
  } else if (error instanceof BehioNetworkError) {
    console.error("Network error:", error.message);
    console.error("Is timeout:", error.code === "TIMEOUT");
  }
}

Matching Specific Error Codes

Use the .is() helper to check for a specific BehioErrorCode:

try {
  await storefront.auth.login({ email, password });
} catch (error) {
  if (error instanceof BehioApiError) {
    if (error.is("INVALID_CREDENTIALS")) {
      showToast("Wrong email or password");
    } else if (error.is("RATE_LIMITED")) {
      showToast("Too many attempts, try again later");
    }
  }
}

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).

try {
  await storefront.checkout.createOrder(input);
} catch (error) {
  if (error instanceof BehioApiError && error.status === 400) {
    // error.body typically contains { message: "...", errors: [...] }
    console.error("Validation failed:", error.body);
  }
}

401 — Unauthorized

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

404 — Not Found

The requested resource does not exist.

try {
  const product = await storefront.catalog.getProduct("nonexistent-slug");
} catch (error) {
  if (error instanceof BehioApiError && error.is("NOT_FOUND")) {
    // Show 404 page or redirect
  }
}

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, the error is thrown.

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

  • Attempt 1 fails → wait 1000ms
  • Attempt 2 fails → wait 2000ms
  • Attempt 3 fails → throw error

Set retries: 0 to disable automatic retries entirely.

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
  • Throws a BehioApiError with status 401
// 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