Error Handling
Handle API errors, retries, and rate limits
Error Types
The SDK throws two error classes, both exported from @behio/storefront-sdk:
| Class | When | isRetryable |
|---|---|---|
BehioApiError | API returned a non-2xx response | true for 5xx and 429 |
BehioNetworkError | Network failure or request timeout | always 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
| Property | Type | Description |
|---|---|---|
status | number | HTTP status code (400, 401, 404, 429, 500, ...) |
code | BehioErrorCode | Semantic error code resolved from status and body |
message | string | Human-readable error message |
body | unknown | Raw response body from the API |
isRetryable | boolean | true for status >= 500 or 429 |
BehioNetworkError
| Property | Type | Description |
|---|---|---|
code | "NETWORK_ERROR" | "TIMEOUT" | Whether it was a timeout or general network failure |
message | string | Error description |
isRetryable | boolean | Always 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 errorCommon 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
- The SDK attempts the request.
- If it fails with a retryable error, it waits
retryDelay * attemptms (linear backoff). - It retries up to
retriestimes. - 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:
- Pauses the failed request
- Calls
auth.refresh()to get a new access token - Retries the original request with the new token
- Emits
auth:token-refreshon success
If the refresh itself fails:
- The SDK clears all tokens
- Emits
auth:token-refresh-failed - Throws a
BehioApiErrorwith 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 resetsRate 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";
}