Error Handling & Troubleshooting
Handle API errors, shopping cart validation, gift card issues, network failures, and debug common integration problems.
Every ResytechApi method returns a response object that extends ApiResponseBase. Errors never throw exceptions -- they are returned as response objects with success: false.
ApiResponseBase
All responses include these fields:
interface ApiResponseBase {
success: boolean; // true if the request succeeded
message: string; // human-readable description
statusCode: number; // HTTP status code (0 for network errors, 408 for timeout)
action: string; // the API endpoint that was called
isNetworkError: boolean; // true for network failures and timeouts
}Error Detection
Check success
The primary way to detect errors:
const response = await api.activity.getActivity('some-uuid');
if (!response.success) {
console.error(`Error: ${response.message}`);
return;
}
// Safe to use response.activityNetwork errors vs API errors
Use isNetworkError to distinguish between server-side failures and connectivity issues:
const response = await api.cart.createOrUpdateCart(request);
if (!response.success) {
if (response.isNetworkError) {
// Connection failed, timeout, or DNS issue
showMessage('Unable to reach the server. Check your connection and try again.');
} else if (response.statusCode >= 500) {
// Server error
showMessage('Something went wrong on our end. Please try again later.');
} else {
// Client error (400, 401, 403, 404, etc.)
showMessage(response.message);
}
}Status codes
| Code | Meaning | isNetworkError |
|---|---|---|
0 | Network failure (fetch threw) | true |
400 | Bad request / validation error | false |
401 | Unauthorized (missing or expired token) | false |
403 | Forbidden | false |
404 | Resource not found | false |
408 | Request timeout (AbortController fired) | true |
500 | Internal server error | false |
Client-side validation errors
Some methods validate input before making the HTTP request. These return immediately with statusCode: 400 and isNetworkError: false:
// This returns an error without making a network request
const response = await api.calendar.getActivityCalendar({
activity: '', // empty
month: 13, // out of range
year: 2025,
});
// response.success === false
// response.message === 'Activity, month, and year are required'
// response.statusCode === 400
// response.isNetworkError === falseShopping Cart Error Codes
When api.cart.createOrUpdateCart() fails, the response includes an errorCode field with a ShoppingCartErrorCode value.
const response = await api.cart.createOrUpdateCart(request);
if (!response.success && response.errorCode !== undefined) {
switch (response.errorCode) {
case 4: // TimeSlotNotAvailable
showMessage('That time slot is no longer available. Please select another.');
break;
case 6: // EquipmentQuantityExceeded
showMessage('Not enough equipment available. Please reduce your quantity.');
break;
case 9: // GuestLimitExceeded
showMessage('Too many guests. Maximum is ' + getGuestLimit());
break;
case 19: // CartExpired
showMessage('Your session has expired. Please start over.');
resetBooking();
break;
default:
showMessage(response.message);
}
}All ShoppingCartErrorCode Values
| Code | Name | Description | Suggested Action |
|---|---|---|---|
0 | None | No error | -- |
1 | ActivityNotFound | Activity UUID is invalid or deleted | Refresh activity list |
2 | EquipmentNotFound | Equipment UUID is invalid or deleted | Refresh activity details |
3 | DurationNotFound | Duration UUID is invalid or deleted | Refresh durations |
4 | TimeSlotNotAvailable | Time slot was booked by someone else | Reload time slots |
5 | EquipmentNotAvailable | Equipment is fully booked for this slot | Show alternative times |
6 | EquipmentQuantityExceeded | Requested more units than available | Show max available in UI |
7 | SeatLimitExceeded | Total seats exceed equipment capacity | Reduce guests or add equipment |
8 | GuestMinimumNotMet | Below minimum guest requirement | Show minimum in UI |
9 | GuestLimitExceeded | Above maximum guest limit | Show maximum in UI |
10 | AddonNotFound | Addon UUID is invalid | Refresh addon list |
11 | AddonNotAvailable | Addon not available for this slot | Remove addon from cart |
12 | AddonQuantityExceeded | Too many addons requested | Reduce addon quantity |
13 | InvalidDate | Date format is wrong or out of range | Validate date input |
14 | DateNotAvailable | Date is blacked out or past cutoff | Reload calendar |
15 | InvalidDuration | Duration selection is not valid | Refresh durations |
16 | InvalidTimeSlot | Time slot data is malformed | Reload time slots |
17 | CustomerRequired | Customer info missing at checkout | Prompt for customer details |
18 | InvalidCustomer | Customer data validation failed | Check email format, required fields |
19 | CartExpired | Cart session timed out | Create a new cart |
20 | CartNotFound | Cart ID does not exist | Create a new cart |
21 | PaymentFailed | Stripe payment was declined | Show payment error, retry |
22 | CouponInvalid | Coupon code not recognized | Show "invalid coupon" message |
23 | CouponExpired | Coupon has expired | Show "coupon expired" message |
24 | GeneralError | Catch-all server error | Show generic error, retry |
Cart Removals
When the cart is updated, the server may remove items that are no longer valid. Check response.removals:
const response = await api.cart.createOrUpdateCart(request);
if (response.success && response.removals?.length) {
response.removals.forEach(removal => {
switch (removal.type) {
case 1: // EquipmentUnavailable
notify(`Equipment is no longer available and was removed.`);
break;
case 2: // EquipmentQuantityReduced
notify(`Equipment quantity reduced to ${removal.maxQuantity}.`);
break;
case 3: // AddonUnavailable
notify(`An addon was removed because it is no longer available.`);
break;
// ... handle other types
}
});
}ShoppingCartRemovalType Values
| Code | Name | Description |
|---|---|---|
0 | None | No removal |
1 | EquipmentUnavailable | Equipment removed |
2 | EquipmentQuantityReduced | Equipment quantity reduced to max |
3 | AddonUnavailable | Addon removed |
4 | AddonQuantityReduced | Addon quantity reduced |
5 | SeatReduction | Seats reduced to max available |
6 | DurationUnavailable | Duration no longer available |
7 | TimeSlotUnavailable | Time slot no longer available |
8 | DateUnavailable | Date no longer available |
9 | ActivityUnavailable | Activity no longer available |
Gift Card Error Codes
When api.cart.applyGiftCard() fails, the response includes a GiftCardErrorCode:
const response = await api.cart.applyGiftCard({
giftCardCode: userInput,
cartId: currentCartId,
});
if (!response.success) {
switch (response.errorCode) {
case 1: // InvalidCode
showError('That gift card code is not valid.');
break;
case 2: // Expired
showError('This gift card has expired.');
break;
case 3: // NoBalance
showError('This gift card has no remaining balance.');
break;
case 5: // AlreadyApplied
showError('This gift card is already applied to your cart.');
break;
case 8: // LocationMismatch
showError('This gift card cannot be used at this location.');
break;
default:
showError(response.message);
}
}All GiftCardErrorCode Values
| Code | Name | Description |
|---|---|---|
0 | None | No error |
1 | InvalidCode | Code format is wrong or unrecognized |
2 | Expired | Gift card has expired |
3 | NoBalance | Balance is zero |
4 | InsufficientBalance | Balance is less than expected (informational) |
5 | AlreadyApplied | Already applied to this cart |
6 | NotFound | Gift card does not exist in the system |
7 | Disabled | Gift cards are disabled for this location |
8 | LocationMismatch | Gift card belongs to a different location |
9 | GeneralError | Catch-all server error |
Network Error Handling
The ApiController uses the Fetch API with AbortController for timeouts. All network-level errors are caught and returned as structured responses.
Timeout
const api = new ResytechApi({ timeout: 15000 }); // 15 seconds
const response = await api.activity.getActivity('uuid');
if (!response.success && response.statusCode === 408) {
// Request timed out
showMessage('The request took too long. Please try again.');
}Connection failure
When fetch() throws (DNS failure, no internet, CORS block), you get:
{
success: false,
message: 'Unable to connect to the server', // or the actual error message
statusCode: 0,
action: '/activity/uuid',
isNetworkError: true
}Retry pattern
async function fetchWithRetry<T>(fn: () => Promise<T & ApiResponseBase>, maxRetries = 3): Promise<T & ApiResponseBase> {
let lastResponse: T & ApiResponseBase;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
lastResponse = await fn();
if (lastResponse.success) return lastResponse;
// Only retry network errors
if (!lastResponse.isNetworkError) return lastResponse;
// Wait before retrying (exponential backoff)
if (attempt < maxRetries) {
await new Promise(r => setTimeout(r, 1000 * attempt));
}
}
return lastResponse!;
}
// Usage
const response = await fetchWithRetry(() => api.activity.getActivity('uuid'));Timeout Configuration
The default timeout is 30 seconds (30000ms). You can configure it globally or rely on the default.
// Short timeout for health checks
const api = new ResytechApi({ timeout: 5000 });
// Check connectivity
const healthy = await api.health.check();
if (!healthy) {
showBanner('Service is temporarily unavailable.');
}The timeout is implemented using AbortController.abort() on the fetch request. When a timeout fires, the response has statusCode: 408 and isNetworkError: true.
Debug Mode
Enable debug mode to log all API requests and responses to the console:
const api = new ResytechApi({ debug: true });Debug output:
[ResytechAPI] POST /cart {...}-- logged before every request[ResytechAPI] Response: {...}-- logged after successful responses[ResytechAPI] Error: {...}-- logged for all error responses[ResytechAPI] Health check failed: ...-- logged for health check failures
Toggle at runtime:
api.setDebug(true); // enable
api.setDebug(false); // disableCORS Considerations
The ResytechApi sends requests with credentials: 'include' to support cookie-based sessions. This means:
- The API server must return
Access-Control-Allow-Originwith your exact origin (not*). - The API server must return
Access-Control-Allow-Credentials: true. - The default API hosts (
api.bookingui.comandcrm-intake.resytech.com) already have CORS configured for standard use.
If you are proxying through your own server or using a custom baseUrl, ensure your server forwards CORS headers correctly.
CORS error symptoms
response.isNetworkError === truewithstatusCode === 0- Browser console shows:
Access to fetch at '...' from origin '...' has been blocked by CORS policy
Fixing CORS issues
- Verify your domain is whitelisted in the Resytech dashboard.
- If using a custom proxy, pass through all
Access-Control-*response headers. - Do not set
mode: 'no-cors'on fetch requests -- this silently drops the response body.
Common Integration Issues
"Location GUID is required" from blog methods
The blog controller requires a location GUID. It is set automatically by initialization.initialize(). If you are using blog methods without initializing first, set it manually:
api.blog.setLocationGuid('your-location-guid');401 Unauthorized after some time
The auth token obtained from initialization.initialize() expires. Re-initialize to get a fresh token:
const response = await api.initialization.initialize({ identifier: 'session-id' });
if (response.success) {
// Token is automatically set on all controllers
}Cart operations fail after page reload
Cart state (including cartId) is not persisted by the library. Store the cartId in sessionStorage or your app state:
const response = await api.cart.createOrUpdateCart({ cart: myCart });
if (response.success) {
sessionStorage.setItem('cartId', response.cartId);
}
// On subsequent requests
const cartId = sessionStorage.getItem('cartId');
await api.cart.applyCoupon({ couponCode: 'SAVE10', cartId });Checkout returns clientSecret but no confirmation
This means Stripe requires additional payment authentication (3D Secure). Use the clientSecret with Stripe.js to handle the next action:
const response = await api.checkout.checkout(request);
if (response.success && response.clientSecret) {
// 3D Secure or additional auth required
const { error, paymentIntent } = await stripe.handleNextAction({
clientSecret: response.clientSecret,
});
if (!error && paymentIntent.status === 'succeeded') {
// Retry checkout with handledNextAction
const finalResponse = await api.checkout.checkout({
...request,
handledNextAction: paymentIntent.id,
});
}
}Dates come back as Date objects, not strings
The ApiController automatically converts ISO date strings in responses to JavaScript Date objects. This applies to fields like firstAvailableDate, publishedAt, createdAt, etc. If you need strings, call .toISOString() on them.
Blog content renders as raw text
If renderer.render() outputs the JSON string instead of HTML, verify the content is valid version 2 block JSON:
const parsed = JSON.parse(post.content);
console.log(parsed.version); // Should be 2
console.log(Array.isArray(parsed.blocks)); // Should be trueIf the content is legacy HTML (not JSON), the renderer passes it through as-is. This is expected behavior for older posts.
