# Error Handling & Troubleshooting (/developers/error-handling)



Every `ResytechApi` method returns a response object that extends `ApiResponseBase`. Errors never throw exceptions -- they are returned as response objects with `success: false`.

ApiResponseBase [#apiresponsebase]

All responses include these fields:

```typescript
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 [#error-detection]

Check success [#check-success]

The primary way to detect errors:

```typescript
const response = await api.activity.getActivity('some-uuid');

if (!response.success) {
  console.error(`Error: ${response.message}`);
  return;
}

// Safe to use response.activity
```

Network errors vs API errors [#network-errors-vs-api-errors]

Use `isNetworkError` to distinguish between server-side failures and connectivity issues:

```typescript
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 [#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 [#client-side-validation-errors]

Some methods validate input before making the HTTP request. These return immediately with `statusCode: 400` and `isNetworkError: false`:

```typescript
// 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 === false
```

Shopping Cart Error Codes [#shopping-cart-error-codes]

When `api.cart.createOrUpdateCart()` fails, the response includes an `errorCode` field with a `ShoppingCartErrorCode` value.

```typescript
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 [#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 [#cart-removals]

When the cart is updated, the server may remove items that are no longer valid. Check `response.removals`:

```typescript
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 [#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 [#gift-card-error-codes]

When `api.cart.applyGiftCard()` fails, the response includes a `GiftCardErrorCode`:

```typescript
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 [#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 [#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 [#timeout]

```typescript
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 [#connection-failure]

When `fetch()` throws (DNS failure, no internet, CORS block), you get:

```typescript
{
  success: false,
  message: 'Unable to connect to the server',  // or the actual error message
  statusCode: 0,
  action: '/activity/uuid',
  isNetworkError: true
}
```

Retry pattern [#retry-pattern]

```typescript
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 [#timeout-configuration]

The default timeout is **30 seconds** (30000ms). You can configure it globally or rely on the default.

```typescript
// 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 [#debug-mode]

Enable debug mode to log all API requests and responses to the console:

```typescript
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:

```typescript
api.setDebug(true);   // enable
api.setDebug(false);  // disable
```

CORS Considerations [#cors-considerations]

The `ResytechApi` sends requests with `credentials: 'include'` to support cookie-based sessions. This means:

1. The API server must return `Access-Control-Allow-Origin` with your exact origin (not `*`).
2. The API server must return `Access-Control-Allow-Credentials: true`.
3. The default API hosts (`api.bookingui.com` and `crm-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 [#cors-error-symptoms]

* `response.isNetworkError === true` with `statusCode === 0`
* Browser console shows: `Access to fetch at '...' from origin '...' has been blocked by CORS policy`

Fixing CORS issues [#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 [#common-integration-issues]

"Location GUID is required" from blog methods [#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:

```typescript
api.blog.setLocationGuid('your-location-guid');
```

401 Unauthorized after some time [#401-unauthorized-after-some-time]

The auth token obtained from `initialization.initialize()` expires. Re-initialize to get a fresh token:

```typescript
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-operations-fail-after-page-reload]

Cart state (including `cartId`) is not persisted by the library. Store the `cartId` in `sessionStorage` or your app state:

```typescript
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 [#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:

```typescript
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 [#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 [#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:

```typescript
const parsed = JSON.parse(post.content);
console.log(parsed.version); // Should be 2
console.log(Array.isArray(parsed.blocks)); // Should be true
```

If the content is legacy HTML (not JSON), the renderer passes it through as-is. This is expected behavior for older posts.
