Skip to main content
This guide covers how to handle SmartMenu API errors gracefully, ensuring a good user experience even when things go wrong.

GraphQL Error Format

SmartMenu API returns errors in the standard GraphQL format:
{
  "data": null,
  "errors": [
    {
      "message": "Missing required header: X-Session-ID",
      "extensions": {
        "code": "MISSING_HEADER",
        "field": "X-Session-ID"
      },
      "path": ["search"]
    }
  ]
}

Error Codes

Authentication Errors

CodeHTTP StatusDescriptionUser Action
MISSING_HEADER400Required header not providedFix request headers
INVALID_TOKEN401API key invalid or expiredCheck API key configuration
FORBIDDEN403Key lacks access to requested chainContact EveryBite

Validation Errors

CodeHTTP StatusDescriptionUser Action
INVALID_INPUT400Request parameter malformedCheck request format
CHAIN_NOT_FOUND404Chain ID doesn’t existVerify chain ID
DISH_NOT_FOUND404Dish ID doesn’t existHandle gracefully

Rate Limiting

CodeHTTP StatusDescriptionUser Action
RATE_LIMITED429Too many requestsWait and retry

Server Errors

CodeHTTP StatusDescriptionUser Action
INTERNAL_ERROR500Server errorRetry or graceful fallback
SERVICE_UNAVAILABLE503Service temporarily downRetry with backoff

Handling Errors in Code

Basic Error Handler

async function smartMenuQuery(query, variables) {
  try {
    const response = await fetch(ENDPOINT, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': API_KEY,
        'X-Session-ID': sessionId,
      },
      body: JSON.stringify({ query, variables })
    });

    // Handle HTTP errors
    if (!response.ok) {
      if (response.status === 429) {
        const retryAfter = response.headers.get('Retry-After') || 5;
        throw new RateLimitError(retryAfter);
      }
      throw new ApiError(response.status, 'API request failed');
    }

    const { data, errors } = await response.json();

    // Handle GraphQL errors
    if (errors && errors.length > 0) {
      const error = errors[0];
      throw new GraphQLError(
        error.message,
        error.extensions?.code
      );
    }

    return data;
  } catch (error) {
    handleError(error);
    throw error;
  }
}

Error Classes

class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = 'ApiError';
  }
}

class GraphQLError extends Error {
  constructor(message: string, public code?: string) {
    super(message);
    this.name = 'GraphQLError';
  }
}

class RateLimitError extends Error {
  constructor(public retryAfter: number) {
    super(`Rate limited. Retry after ${retryAfter} seconds.`);
    this.name = 'RateLimitError';
  }
}

Centralized Error Handler

function handleError(error: Error) {
  if (error instanceof RateLimitError) {
    // Show temporary message, auto-retry
    showToast(`Please wait ${error.retryAfter} seconds...`);
    return;
  }

  if (error instanceof GraphQLError) {
    switch (error.code) {
      case 'DISH_NOT_FOUND':
        showToast('This dish is no longer available');
        break;
      case 'CHAIN_NOT_FOUND':
        showToast('Restaurant not found');
        break;
      case 'INVALID_TOKEN':
        // This shouldn't happen in production
        console.error('Invalid API key');
        break;
      default:
        showToast('Something went wrong. Please try again.');
    }
    return;
  }

  // Network or unexpected errors
  showToast('Unable to load menu. Please check your connection.');
}

Graceful Degradation

When SmartMenu API is unavailable, your app should continue to function.

Fallback Pattern

async function getDishDetails(dishId) {
  try {
    // Try SmartMenu API
    const data = await smartMenuQuery(DISH_QUERY, { id: dishId });
    return {
      ...data.dish,
      nutritionAvailable: true
    };
  } catch (error) {
    console.error('SmartMenu API error:', error);

    // Fall back to your own data (without nutrition)
    const localDish = await getLocalDishData(dishId);
    return {
      ...localDish,
      nutritionAvailable: false,
      nutrition: null,
      allergens: [],
      diets: []
    };
  }
}

UI Fallback

function DishDetails({ dish }) {
  if (!dish.nutritionAvailable) {
    return (
      <div className="dish-details">
        <h2>{dish.name}</h2>
        <p>{dish.description}</p>

        <div className="nutrition-unavailable">
          <InfoIcon />
          <span>Nutrition information temporarily unavailable</span>
        </div>
      </div>
    );
  }

  return (
    <div className="dish-details">
      <h2>{dish.name}</h2>
      <p>{dish.description}</p>
      <NutritionPanel nutrition={dish.nutrition} />
      <AllergenBadges allergens={dish.allergens} />
    </div>
  );
}

Retry Logic

Exponential Backoff

async function fetchWithRetry(
  fn: () => Promise<any>,
  maxRetries = 3,
  baseDelay = 1000
) {
  let lastError: Error;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      // Don't retry client errors (4xx)
      if (error instanceof ApiError && error.status < 500) {
        throw error;
      }

      // Don't retry on last attempt
      if (attempt === maxRetries - 1) {
        throw error;
      }

      // Exponential backoff with jitter
      const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
      await sleep(delay);
    }
  }

  throw lastError;
}

// Usage
const data = await fetchWithRetry(() =>
  smartMenuQuery(SEARCH_QUERY, variables)
);

Rate Limit Handling

async function fetchWithRateLimitRetry(fn: () => Promise<any>) {
  try {
    return await fn();
  } catch (error) {
    if (error instanceof RateLimitError) {
      // Wait for the specified time
      await sleep(error.retryAfter * 1000);
      // Retry once
      return await fn();
    }
    throw error;
  }
}

User-Facing Error Messages

Error CodeUser Message
RATE_LIMITED”Please wait a moment and try again”
DISH_NOT_FOUND”This dish is no longer available”
CHAIN_NOT_FOUND”Restaurant not found”
INTERNAL_ERROR”Something went wrong. Please try again.”
Network Error”Unable to connect. Please check your internet connection.”

Logging and Monitoring

function logError(error: Error, context: object) {
  const errorLog = {
    timestamp: new Date().toISOString(),
    name: error.name,
    message: error.message,
    code: error instanceof GraphQLError ? error.code : undefined,
    ...context
  };

  // Send to your logging service
  console.error('SmartMenu API Error:', errorLog);

  // Track in analytics
  analytics.track('smartmenu_error', {
    error_type: error.name,
    error_code: error instanceof GraphQLError ? error.code : 'unknown',
    ...context
  });
}

Best Practices

Always Handle Errors

Never let errors bubble up to crash your app. Always catch and handle.

User-Friendly Messages

Don’t show technical error messages to users. Translate to helpful guidance.

Fail Gracefully

If nutrition isn’t available, the dish should still be orderable.

Log for Debugging

Log errors with context for debugging, but don’t expose details to users.