Error Handling

Handle errors gracefully in your application

This guide covers error handling patterns for the Arky SDK.

Result Type

All SDK methods return a Result type that explicitly handles success and failure:

const result = await sdk.eshop.getProduct({
  businessId: 'biz_abc123',
  id: 'prod_xyz'
});

if (result.ok) {
  // Success - result.val contains the data
  const product = result.val;
  console.log(product.name);
} else {
  // Error - result.val contains error details
  const error = result.val;
  console.error(error.message);
}

Error Structure

Errors follow a consistent structure:

interface ApiError {
  error: string;      // Error code (e.g., "NOT_FOUND")
  message: string;    // Human-readable message
  details?: {         // Additional context
    field?: string;
    reason?: string;
  };
  statusCode: number; // HTTP status code
}

Common Error Codes

Authentication Errors (401)

CodeDescription
UNAUTHORIZEDMissing or invalid token
TOKEN_EXPIREDAccess token has expired
INVALID_CREDENTIALSWrong email/password
USER_NOT_CONFIRMEDEmail not verified
USER_DISABLEDAccount has been disabled

Authorization Errors (403)

CodeDescription
FORBIDDENInsufficient permissions
BUSINESS_ACCESS_DENIEDNo access to this business

Not Found Errors (404)

CodeDescription
NOT_FOUNDResource doesn’t exist
PRODUCT_NOT_FOUNDProduct not found
ORDER_NOT_FOUNDOrder not found
USER_NOT_FOUNDUser not found

Validation Errors (400)

CodeDescription
VALIDATION_ERRORInvalid input data
INVALID_EMAILInvalid email format
WEAK_PASSWORDPassword too weak
MISSING_FIELDRequired field missing
INVALID_PROMO_CODEPromo code invalid or expired

Conflict Errors (409)

CodeDescription
DUPLICATE_EMAILEmail already registered
DUPLICATE_SLUGSlug already in use
SLOT_UNAVAILABLETime slot no longer available
INSUFFICIENT_INVENTORYNot enough stock

Payment Errors (402)

CodeDescription
PAYMENT_FAILEDPayment processing failed
CARD_DECLINEDCard was declined
INSUFFICIENT_FUNDSInsufficient funds
REQUIRES_ACTION3D Secure required

Handling Patterns

Basic Error Handling

async function getProduct(id: string) {
  const result = await sdk.eshop.getProduct({
    businessId: 'biz_abc123',
    id
  });

  if (!result.ok) {
    const error = result.val;

    switch (error.error) {
      case 'NOT_FOUND':
        throw new NotFoundError('Product not found');
      case 'UNAUTHORIZED':
        redirectToLogin();
        return null;
      default:
        throw new Error(error.message);
    }
  }

  return result.val;
}

Form Validation Errors

async function handleRegister(formData: RegisterForm) {
  const result = await sdk.user.registerUser({
    ...formData,
    provider: 'EMAIL'
  });

  if (!result.ok) {
    const error = result.val;

    // Map API errors to form fields
    if (error.error === 'VALIDATION_ERROR') {
      return {
        success: false,
        fieldErrors: {
          [error.details?.field || 'form']: error.message
        }
      };
    }

    if (error.error === 'DUPLICATE_EMAIL') {
      return {
        success: false,
        fieldErrors: {
          email: 'This email is already registered'
        }
      };
    }

    return {
      success: false,
      formError: error.message
    };
  }

  return { success: true, user: result.val };
}

React Error Component

function useApiCall<T>(
  apiCall: () => Promise<Result<T, ApiError>>
) {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<ApiError | null>(null);
  const [loading, setLoading] = useState(false);

  const execute = async () => {
    setLoading(true);
    setError(null);

    const result = await apiCall();

    if (result.ok) {
      setData(result.val);
    } else {
      setError(result.val);
    }

    setLoading(false);
  };

  return { data, error, loading, execute };
}

// Usage
function ProductPage({ id }) {
  const { data: product, error, loading, execute } = useApiCall(() =>
    sdk.eshop.getProduct({ businessId: 'biz_123', id })
  );

  useEffect(() => { execute(); }, [id]);

  if (loading) return <Spinner />;
  if (error) return <ErrorDisplay error={error} onRetry={execute} />;
  if (!product) return null;

  return <Product data={product} />;
}

function ErrorDisplay({ error, onRetry }) {
  return (
    <div className="error-container">
      <h2>Something went wrong</h2>
      <p>{error.message}</p>
      {onRetry && (
        <button onClick={onRetry}>Try Again</button>
      )}
    </div>
  );
}

Global Error Handler

// Create SDK with error handling
const sdk = createArkyClient({
  businessId: process.env.ARKY_BUSINESS_ID!,
  getToken: () => localStorage.getItem('arky_token'),
  setToken: (token) => localStorage.setItem('arky_token', token),

  onAuthError: () => {
    // Clear token and redirect to login
    localStorage.removeItem('arky_token');
    window.location.href = '/login?expired=true';
  }
});

Retry Logic

async function withRetry<T>(
  fn: () => Promise<Result<T, ApiError>>,
  maxRetries = 3,
  delay = 1000
): Promise<Result<T, ApiError>> {
  let lastError: ApiError | null = null;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const result = await fn();

    if (result.ok) return result;

    lastError = result.val;

    // Don't retry client errors
    if (lastError.statusCode >= 400 && lastError.statusCode < 500) {
      return result;
    }

    // Wait before retrying
    await new Promise(resolve => setTimeout(resolve, delay * (attempt + 1)));
  }

  return { ok: false, val: lastError! };
}

// Usage
const result = await withRetry(() =>
  sdk.eshop.getProducts({ businessId: 'biz_123', limit: 20 })
);

User-Friendly Messages

Map error codes to user-friendly messages:

const ERROR_MESSAGES: Record<string, string> = {
  // Auth
  INVALID_CREDENTIALS: 'Invalid email or password. Please try again.',
  USER_NOT_CONFIRMED: 'Please check your email to verify your account.',
  TOKEN_EXPIRED: 'Your session has expired. Please log in again.',

  // Products
  PRODUCT_NOT_FOUND: 'This product is no longer available.',
  INSUFFICIENT_INVENTORY: 'Sorry, this item is out of stock.',

  // Orders
  PAYMENT_FAILED: 'Payment failed. Please check your card details.',
  CARD_DECLINED: 'Your card was declined. Please try another payment method.',
  INVALID_PROMO_CODE: 'This promo code is invalid or has expired.',

  // Reservations
  SLOT_UNAVAILABLE: 'This time slot is no longer available. Please choose another.',

  // Default
  DEFAULT: 'Something went wrong. Please try again later.'
};

function getErrorMessage(error: ApiError): string {
  return ERROR_MESSAGES[error.error] || ERROR_MESSAGES.DEFAULT;
}

Logging Errors

Log errors for debugging:

function logError(error: ApiError, context?: Record<string, unknown>) {
  console.error('API Error:', {
    code: error.error,
    message: error.message,
    status: error.statusCode,
    details: error.details,
    ...context
  });

  // Send to error tracking service
  if (process.env.NODE_ENV === 'production') {
    errorTracker.capture(error, context);
  }
}

// Usage
const result = await sdk.eshop.checkout({ ... });

if (!result.ok) {
  logError(result.val, {
    orderId: order.id,
    userId: currentUser.id
  });
}
Tip

Always provide clear, actionable error messages to users. Technical details should be logged but not displayed.

Testing Error Scenarios

// Test error handling
describe('Product Page', () => {
  it('shows error message when product not found', async () => {
    // Mock API to return error
    vi.spyOn(sdk.eshop, 'getProduct').mockResolvedValue({
      ok: false,
      val: {
        error: 'NOT_FOUND',
        message: 'Product not found',
        statusCode: 404
      }
    });

    render(<ProductPage id="invalid" />);

    await waitFor(() => {
      expect(screen.getByText(/not found/i)).toBeInTheDocument();
    });
  });
});