Documentation Index
Fetch the complete documentation index at: https://docs.fanfare.io/llms.txt
Use this file to discover all available pages before exploring further.
Error Handling Guide
Learn how to handle errors gracefully in your Fanfare integration, from SDK errors to network failures.Overview
Robust error handling ensures a good user experience even when things go wrong. This guide covers error types, handling strategies, and recovery patterns for Fanfare integrations. What you’ll learn:- Understanding Fanfare error types
- Implementing client-side error handling
- Server-side error management
- User-friendly error messages
- Retry and recovery strategies
Prerequisites
- Working Fanfare integration
- Understanding of async/await and Promises
- Basic error handling concepts
- Logging infrastructure (recommended)
Error Categories
| Category | Examples | Recovery |
|---|---|---|
| Network Errors | Timeout, connection refused | Retry with backoff |
| Authentication | Invalid token, expired session | Re-authenticate |
| Validation | Invalid input, missing fields | Show user feedback |
| Business Logic | Admission expired, sold out | Show clear message |
| Server Errors | 500 errors, service unavailable | Retry or show status |
Step 1: SDK Error Types
Error Class Hierarchy
// Fanfare SDK error types
import {
FanfareError,
AuthenticationError,
ValidationError,
NetworkError,
APIError,
} from "@waitify-io/fanfare-sdk-core";
// Base error class
class FanfareError extends Error {
code: string;
details?: Record<string, unknown>;
constructor(message: string, code: string, details?: Record<string, unknown>) {
super(message);
this.name = "FanfareError";
this.code = code;
this.details = details;
}
}
// Specific error types
class AuthenticationError extends FanfareError {
constructor(message: string, code: string = "AUTH_ERROR") {
super(message, code);
this.name = "AuthenticationError";
}
}
class ValidationError extends FanfareError {
constructor(message: string, code: string = "VALIDATION_ERROR", details?: Record<string, unknown>) {
super(message, code, details);
this.name = "ValidationError";
}
}
class NetworkError extends FanfareError {
constructor(message: string, code: string = "NETWORK_ERROR") {
super(message, code);
this.name = "NetworkError";
}
}
class APIError extends FanfareError {
status: number;
constructor(message: string, status: number, code: string) {
super(message, code);
this.name = "APIError";
this.status = status;
}
}
Common Error Codes
| Code | Meaning | User Action |
|---|---|---|
AUTH_REQUIRED | Not authenticated | Sign in |
AUTH_EXPIRED | Session expired | Re-authenticate |
ADMISSION_EXPIRED | Checkout window closed | Return to queue |
ADMISSION_INVALID | Token invalid/used | Return to queue |
EXPERIENCE_NOT_FOUND | Experience doesn’t exist | Check URL |
EXPERIENCE_ENDED | Experience concluded | Show ended state |
RATE_LIMITED | Too many requests | Wait and retry |
SOLD_OUT | No inventory | Show sold out |
CONSUMER_LIMIT | Max purchases reached | Explain limit |
Step 2: Client-Side Error Handling
React Error Boundary
// components/ErrorBoundary.tsx
import React, { Component, ReactNode } from "react";
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class FanfareErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log to error tracking service
console.error("Fanfare error:", error, errorInfo);
this.props.onError?.(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
this.props.fallback || (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>We're having trouble loading this experience.</p>
<button onClick={() => window.location.reload()}>Try Again</button>
</div>
)
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<FanfareErrorBoundary
fallback={<ExperienceErrorFallback />}
onError={(error) => {
analytics.trackError("fanfare_error", { error: error.message });
}}
>
<FanfareProvider organizationId="your-org-id">
<YourApp />
</FanfareProvider>
</FanfareErrorBoundary>
);
}
Hook-Level Error Handling
// hooks/useFanfareWithErrorHandling.ts
import { useExperienceJourney } from "@waitify-io/fanfare-sdk-react";
import { useState, useCallback } from "react";
interface ErrorState {
error: Error | null;
isRetrying: boolean;
retryCount: number;
}
export function useFanfareWithErrorHandling(experienceId: string) {
const { journey, state, start, status } = useExperienceJourney(experienceId);
const [errorState, setErrorState] = useState<ErrorState>({
error: null,
isRetrying: false,
retryCount: 0,
});
const handleError = useCallback(
(error: Error) => {
setErrorState((prev) => ({
...prev,
error,
isRetrying: false,
}));
// Categorize and log error
logError(error);
// Check if recoverable
if (isRecoverableError(error)) {
// Auto-retry for certain errors
if (errorState.retryCount < 3) {
setTimeout(() => retry(), getRetryDelay(errorState.retryCount));
}
}
},
[errorState.retryCount]
);
const retry = useCallback(async () => {
setErrorState((prev) => ({
...prev,
isRetrying: true,
retryCount: prev.retryCount + 1,
}));
try {
await start();
setErrorState({ error: null, isRetrying: false, retryCount: 0 });
} catch (error) {
handleError(error instanceof Error ? error : new Error("Unknown error"));
}
}, [start, handleError]);
const clearError = useCallback(() => {
setErrorState({ error: null, isRetrying: false, retryCount: 0 });
}, []);
return {
journey,
state,
status,
error: errorState.error,
isRetrying: errorState.isRetrying,
retry,
clearError,
};
}
function isRecoverableError(error: Error): boolean {
if (error instanceof NetworkError) return true;
if (error instanceof APIError && error.status >= 500) return true;
if (error.message.includes("timeout")) return true;
return false;
}
function getRetryDelay(retryCount: number): number {
// Exponential backoff: 1s, 2s, 4s
return Math.min(1000 * Math.pow(2, retryCount), 10000);
}
function logError(error: Error) {
const errorData = {
name: error.name,
message: error.message,
code: (error as FanfareError).code,
stack: error.stack,
timestamp: new Date().toISOString(),
};
console.error("Fanfare error:", errorData);
// Send to error tracking service
if (typeof window !== "undefined" && window.Sentry) {
window.Sentry.captureException(error);
}
}
Error Display Components
// components/ErrorDisplay.tsx
import { FanfareError, AuthenticationError, NetworkError, APIError } from "@waitify-io/fanfare-sdk-core";
interface ErrorDisplayProps {
error: Error;
onRetry?: () => void;
onDismiss?: () => void;
}
export function ErrorDisplay({ error, onRetry, onDismiss }: ErrorDisplayProps) {
const errorInfo = getErrorInfo(error);
return (
<div className={`error-display ${errorInfo.severity}`} role="alert">
<div className="error-icon">{errorInfo.icon}</div>
<div className="error-content">
<h3 className="error-title">{errorInfo.title}</h3>
<p className="error-message">{errorInfo.message}</p>
{errorInfo.suggestion && <p className="error-suggestion">{errorInfo.suggestion}</p>}
</div>
<div className="error-actions">
{errorInfo.canRetry && onRetry && <button onClick={onRetry}>Try Again</button>}
{errorInfo.action && <button onClick={errorInfo.action.handler}>{errorInfo.action.label}</button>}
{onDismiss && (
<button onClick={onDismiss} className="dismiss">
Dismiss
</button>
)}
</div>
</div>
);
}
interface ErrorInfo {
title: string;
message: string;
suggestion?: string;
severity: "error" | "warning" | "info";
icon: string;
canRetry: boolean;
action?: {
label: string;
handler: () => void;
};
}
function getErrorInfo(error: Error): ErrorInfo {
// Authentication errors
if (error instanceof AuthenticationError) {
return {
title: "Session Expired",
message: "Your session has expired. Please sign in again.",
severity: "warning",
icon: "🔐",
canRetry: false,
action: {
label: "Sign In",
handler: () => (window.location.href = "/login"),
},
};
}
// Network errors
if (error instanceof NetworkError) {
return {
title: "Connection Problem",
message: "We're having trouble connecting. Please check your internet connection.",
severity: "warning",
icon: "📡",
canRetry: true,
};
}
// API errors
if (error instanceof APIError) {
return getAPIErrorInfo(error);
}
// Fanfare-specific errors
if (error instanceof FanfareError) {
return getFanfareErrorInfo(error);
}
// Generic error
return {
title: "Something Went Wrong",
message: "An unexpected error occurred. Please try again.",
severity: "error",
icon: "⚠️",
canRetry: true,
};
}
function getAPIErrorInfo(error: APIError): ErrorInfo {
switch (error.status) {
case 401:
return {
title: "Not Authorized",
message: "You need to sign in to continue.",
severity: "warning",
icon: "🔒",
canRetry: false,
action: {
label: "Sign In",
handler: () => (window.location.href = "/login"),
},
};
case 403:
return {
title: "Access Denied",
message: "You don't have permission to access this resource.",
severity: "error",
icon: "🚫",
canRetry: false,
};
case 404:
return {
title: "Not Found",
message: "The requested resource could not be found.",
severity: "warning",
icon: "🔍",
canRetry: false,
};
case 429:
return {
title: "Too Many Requests",
message: "Please wait a moment before trying again.",
suggestion: "We're processing a lot of requests right now.",
severity: "warning",
icon: "⏳",
canRetry: true,
};
case 500:
case 502:
case 503:
return {
title: "Server Error",
message: "We're experiencing technical difficulties. Please try again shortly.",
severity: "error",
icon: "🔧",
canRetry: true,
};
default:
return {
title: "Request Failed",
message: error.message || "An error occurred while processing your request.",
severity: "error",
icon: "❌",
canRetry: true,
};
}
}
function getFanfareErrorInfo(error: FanfareError): ErrorInfo {
switch (error.code) {
case "ADMISSION_EXPIRED":
return {
title: "Session Expired",
message: "Your checkout window has expired.",
suggestion: "Return to the experience to try again.",
severity: "warning",
icon: "⏱️",
canRetry: false,
action: {
label: "Return to Experience",
handler: () => window.history.back(),
},
};
case "ADMISSION_INVALID":
return {
title: "Invalid Access",
message: "Your admission is no longer valid.",
suggestion: "This may happen if you've already completed a purchase.",
severity: "warning",
icon: "🎫",
canRetry: false,
};
case "EXPERIENCE_ENDED":
return {
title: "Experience Ended",
message: "This experience has concluded.",
severity: "info",
icon: "🏁",
canRetry: false,
};
case "SOLD_OUT":
return {
title: "Sold Out",
message: "This item is no longer available.",
suggestion: "Join the waitlist to be notified if more become available.",
severity: "info",
icon: "😔",
canRetry: false,
};
case "CONSUMER_LIMIT":
return {
title: "Limit Reached",
message: "You've reached the maximum allowed purchases for this experience.",
severity: "info",
icon: "📊",
canRetry: false,
};
default:
return {
title: "Error",
message: error.message,
severity: "error",
icon: "⚠️",
canRetry: true,
};
}
}
Step 3: Server-Side Error Handling
Express Error Middleware
// middleware/error-handler.ts
import { Request, Response, NextFunction } from "express";
import { FanfareAPIError } from "../services/fanfare-api";
interface ErrorResponse {
error: {
code: string;
message: string;
details?: Record<string, unknown>;
};
requestId: string;
}
export function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
const requestId = req.headers["x-request-id"] || generateRequestId();
// Log error
console.error("Error:", {
requestId,
error: err.message,
stack: err.stack,
path: req.path,
method: req.method,
});
// Determine response
const response = getErrorResponse(err, requestId);
res.status(response.status).json(response.body);
}
function getErrorResponse(err: Error, requestId: string): { status: number; body: ErrorResponse } {
// Fanfare API errors
if (err instanceof FanfareAPIError) {
return {
status: err.status,
body: {
error: {
code: err.code,
message: getPublicMessage(err),
},
requestId,
},
};
}
// Validation errors
if (err.name === "ValidationError") {
return {
status: 400,
body: {
error: {
code: "VALIDATION_ERROR",
message: err.message,
details: (err as ValidationError).details,
},
requestId,
},
};
}
// Default to 500
return {
status: 500,
body: {
error: {
code: "INTERNAL_ERROR",
message: "An unexpected error occurred",
},
requestId,
},
};
}
function getPublicMessage(err: FanfareAPIError): string {
// Map internal errors to user-friendly messages
const publicMessages: Record<string, string> = {
ADMISSION_EXPIRED: "Your checkout session has expired",
ADMISSION_INVALID: "Your access is no longer valid",
EXPERIENCE_ENDED: "This experience has ended",
SOLD_OUT: "This item is sold out",
RATE_LIMITED: "Please wait a moment and try again",
};
return publicMessages[err.code] || err.message;
}
function generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
Retry Middleware
// middleware/retry.ts
import { Request, Response, NextFunction } from "express";
interface RetryOptions {
maxRetries: number;
retryableStatuses: number[];
baseDelay: number;
}
export function withRetry(
handler: (req: Request, res: Response) => Promise<void>,
options: RetryOptions = {
maxRetries: 3,
retryableStatuses: [502, 503, 504],
baseDelay: 1000,
}
) {
return async (req: Request, res: Response, next: NextFunction) => {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
try {
await handler(req, res);
return;
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
const isRetryable = isRetryableError(error, options.retryableStatuses);
const hasMoreRetries = attempt < options.maxRetries;
if (isRetryable && hasMoreRetries) {
const delay = options.baseDelay * Math.pow(2, attempt);
await sleep(delay);
continue;
}
break;
}
}
next(lastError);
};
}
function isRetryableError(error: unknown, retryableStatuses: number[]): boolean {
if (error instanceof FanfareAPIError) {
return retryableStatuses.includes(error.status);
}
if (error instanceof Error && error.message.includes("ECONNRESET")) {
return true;
}
return false;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Step 4: Error Recovery Strategies
Automatic Retry with Backoff
// utils/retry.ts
interface RetryConfig {
maxAttempts: number;
baseDelay: number;
maxDelay: number;
shouldRetry?: (error: Error, attempt: number) => boolean;
onRetry?: (error: Error, attempt: number) => void;
}
export async function withRetry<T>(fn: () => Promise<T>, config: RetryConfig): Promise<T> {
const { maxAttempts, baseDelay, maxDelay, shouldRetry, onRetry } = config;
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
const canRetry = shouldRetry?.(lastError, attempt) ?? isDefaultRetryable(lastError);
if (!canRetry || attempt === maxAttempts) {
throw lastError;
}
onRetry?.(lastError, attempt);
// Calculate delay with jitter
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
const jitter = delay * 0.1 * Math.random();
await sleep(delay + jitter);
}
}
throw lastError!;
}
function isDefaultRetryable(error: Error): boolean {
// Network errors
if (error instanceof NetworkError) return true;
// Server errors (5xx)
if (error instanceof APIError && error.status >= 500) return true;
// Specific error codes
if (error instanceof FanfareError) {
return ["RATE_LIMITED", "TIMEOUT", "SERVICE_UNAVAILABLE"].includes(error.code);
}
return false;
}
// Usage
const result = await withRetry(() => fanfareAPI.completeAdmission(params), {
maxAttempts: 3,
baseDelay: 1000,
maxDelay: 10000,
shouldRetry: (error) => !(error instanceof ValidationError),
onRetry: (error, attempt) => {
console.log(`Retry attempt ${attempt}:`, error.message);
},
});
Graceful Degradation
// components/ExperienceWithFallback.tsx
import { useState, useEffect } from "react";
function ExperienceWithFallback({ experienceId }: { experienceId: string }) {
const [mode, setMode] = useState<"full" | "degraded" | "offline">("full");
const { state, error, retry } = useFanfareWithErrorHandling(experienceId);
useEffect(() => {
if (error) {
if (error instanceof NetworkError) {
setMode("offline");
} else if (error instanceof APIError && error.status >= 500) {
setMode("degraded");
}
} else {
setMode("full");
}
}, [error]);
switch (mode) {
case "full":
return <FullExperience state={state} />;
case "degraded":
return <DegradedExperience message="Some features may be limited" onRetry={retry} />;
case "offline":
return (
<OfflineMode
onReconnect={() => {
retry();
setMode("full");
}}
/>
);
}
}
function DegradedExperience({ message, onRetry }: { message: string; onRetry: () => void }) {
return (
<div className="degraded-experience">
<div className="warning-banner">{message}</div>
{/* Show cached/static content */}
<StaticExperienceInfo />
<button onClick={onRetry}>Check Connection</button>
</div>
);
}
function OfflineMode({ onReconnect }: { onReconnect: () => void }) {
return (
<div className="offline-mode">
<div className="offline-icon">📡</div>
<h2>You're Offline</h2>
<p>Check your internet connection and try again.</p>
<button onClick={onReconnect}>Reconnect</button>
</div>
);
}
Circuit Breaker Pattern
// utils/circuit-breaker.ts
type CircuitState = "closed" | "open" | "half-open";
interface CircuitBreakerConfig {
failureThreshold: number;
resetTimeout: number;
halfOpenRequests: number;
}
export class CircuitBreaker {
private state: CircuitState = "closed";
private failures = 0;
private lastFailure: number | null = null;
private halfOpenAttempts = 0;
constructor(private config: CircuitBreakerConfig) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === "open") {
if (this.shouldAttemptReset()) {
this.state = "half-open";
this.halfOpenAttempts = 0;
} else {
throw new CircuitOpenError("Circuit breaker is open");
}
}
if (this.state === "half-open") {
if (this.halfOpenAttempts >= this.config.halfOpenRequests) {
throw new CircuitOpenError("Circuit breaker is half-open, waiting for reset");
}
this.halfOpenAttempts++;
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = "closed";
}
private onFailure() {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.config.failureThreshold) {
this.state = "open";
}
}
private shouldAttemptReset(): boolean {
if (!this.lastFailure) return true;
return Date.now() - this.lastFailure >= this.config.resetTimeout;
}
getState(): CircuitState {
return this.state;
}
}
class CircuitOpenError extends Error {
constructor(message: string) {
super(message);
this.name = "CircuitOpenError";
}
}
// Usage
const fanfareCircuit = new CircuitBreaker({
failureThreshold: 5,
resetTimeout: 30000, // 30 seconds
halfOpenRequests: 3,
});
async function safeFanfareCall<T>(fn: () => Promise<T>): Promise<T> {
try {
return await fanfareCircuit.execute(fn);
} catch (error) {
if (error instanceof CircuitOpenError) {
// Show degraded experience
throw new Error("Service temporarily unavailable");
}
throw error;
}
}
Step 5: Logging and Monitoring
Error Logging Service
// services/error-logger.ts
interface ErrorLog {
timestamp: string;
level: "error" | "warn" | "info";
error: {
name: string;
message: string;
code?: string;
stack?: string;
};
context: {
experienceId?: string;
consumerId?: string;
stage?: string;
action?: string;
};
metadata: Record<string, unknown>;
}
export class ErrorLogger {
private static instance: ErrorLogger;
private queue: ErrorLog[] = [];
private flushInterval: NodeJS.Timeout | null = null;
private constructor() {
// Flush queue periodically
this.flushInterval = setInterval(() => this.flush(), 10000);
// Flush on page unload
if (typeof window !== "undefined") {
window.addEventListener("beforeunload", () => this.flush());
}
}
static getInstance(): ErrorLogger {
if (!ErrorLogger.instance) {
ErrorLogger.instance = new ErrorLogger();
}
return ErrorLogger.instance;
}
log(
level: ErrorLog["level"],
error: Error,
context: ErrorLog["context"] = {},
metadata: Record<string, unknown> = {}
) {
const logEntry: ErrorLog = {
timestamp: new Date().toISOString(),
level,
error: {
name: error.name,
message: error.message,
code: (error as FanfareError).code,
stack: error.stack,
},
context,
metadata: {
...metadata,
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
url: typeof window !== "undefined" ? window.location.href : undefined,
},
};
this.queue.push(logEntry);
// Immediate flush for errors
if (level === "error") {
this.flush();
}
}
private async flush() {
if (this.queue.length === 0) return;
const entries = [...this.queue];
this.queue = [];
try {
await fetch("/api/logs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ entries }),
});
} catch (error) {
// Re-queue on failure (with limit)
if (this.queue.length < 100) {
this.queue.unshift(...entries);
}
}
}
}
// Usage
const logger = ErrorLogger.getInstance();
try {
await journey.start();
} catch (error) {
logger.log(
"error",
error,
{
experienceId: "exp_123",
action: "journey_start",
},
{
attemptNumber: 1,
}
);
}
Monitoring Dashboard Metrics
// utils/metrics.ts
interface Metric {
name: string;
value: number;
tags: Record<string, string>;
timestamp: number;
}
export class MetricsCollector {
private metrics: Metric[] = [];
increment(name: string, tags: Record<string, string> = {}) {
this.record(name, 1, tags);
}
record(name: string, value: number, tags: Record<string, string> = {}) {
this.metrics.push({
name,
value,
tags,
timestamp: Date.now(),
});
}
timing(name: string, duration: number, tags: Record<string, string> = {}) {
this.record(`${name}_duration_ms`, duration, tags);
}
async flush() {
if (this.metrics.length === 0) return;
const data = [...this.metrics];
this.metrics = [];
await fetch("/api/metrics", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ metrics: data }),
});
}
}
// Track errors
const metrics = new MetricsCollector();
function trackError(error: Error, context: Record<string, string>) {
metrics.increment("fanfare_error", {
error_type: error.name,
error_code: (error as FanfareError).code || "unknown",
...context,
});
}
Best Practices
1. Never Show Raw Errors
// Bad
catch (error) {
alert(error.message); // "TypeError: Cannot read property 'x' of undefined"
}
// Good
catch (error) {
const friendlyMessage = getErrorMessage(error);
showNotification(friendlyMessage); // "Something went wrong. Please try again."
}
2. Preserve Error Context
// Wrap errors with context
class ContextualError extends Error {
constructor(
message: string,
public readonly cause: Error,
public readonly context: Record<string, unknown>
) {
super(message);
this.name = "ContextualError";
}
}
try {
await processAdmission(token);
} catch (error) {
throw new ContextualError("Failed to process admission", error as Error, {
token,
consumerId,
experienceId,
});
}
3. Fail Fast for Unrecoverable Errors
function validateAdmissionToken(token: string) {
if (!token) {
throw new ValidationError("Admission token is required");
}
if (token.length !== 32) {
throw new ValidationError("Invalid admission token format");
}
// Continue with valid token
}
Troubleshooting
Error Not Being Caught
- Check for unhandled promise rejections
- Verify error boundary placement
- Review async/await usage
Retry Loop
- Implement max retry limit
- Use circuit breaker pattern
- Add jitter to prevent thundering herd
User Sees Technical Errors
- Implement error message mapping
- Review error boundary fallbacks
- Test error scenarios in QA
What’s Next
- Webhooks Guide - Handle server-side errors
- Real-time Updates - Connection error handling
- Custom Platform - End-to-end error handling