Skip to main content

Overview

The ZKScore SDK provides detailed error information to help you build robust applications. This guide covers error types, handling strategies, and best practices.

Error Structure

All SDK errors follow a consistent structure:
{
  code: 'ERROR_CODE',
  message: 'Human-readable error message',
  details: {
    // Additional context
  },
  statusCode: 400,
  timestamp: '2024-10-23T14:30:00Z'
}

Common Error Codes

Authentication Errors

Code: INVALID_API_KEY
Status: 401
The provided API key is invalid or has been revoked.
try {
  await sdk.scores.getScore(address);
} catch (error) {
  if (error.code === 'INVALID_API_KEY') {
    console.log('Please check your API key');
    // Redirect to API key management
  }
}
Code: API_KEY_EXPIRED
Status: 401
The API key has expired and needs to be renewed.
Code: UNAUTHORIZED
Status: 403
The API key doesn’t have permission for this operation.

Resource Errors

Code: IDENTITY_NOT_FOUND
Status: 404
The address doesn’t have a ZKScore identity.
try {
  const identity = await sdk.identity.getIdentity(address);
} catch (error) {
  if (error.code === 'IDENTITY_NOT_FOUND') {
    // Prompt user to create identity
    const newIdentity = await sdk.identity.mint({
      address,
      username: 'alice',
    });
  }
}
Code: USERNAME_TAKEN
Status: 409
The requested username is already in use.
Code: ACHIEVEMENT_NOT_FOUND
Status: 404
The specified achievement doesn’t exist.
Code: ATTESTATION_NOT_FOUND
Status: 404
The attestation ID doesn’t exist or has been revoked.

Rate Limiting

Code: RATE_LIMIT_EXCEEDED
Status: 429
Too many requests. Includes retry-after information.
try {
  await sdk.scores.getScore(address);
} catch (error) {
  if (error.code === 'RATE_LIMIT_EXCEEDED') {
    const retryAfter = error.details.retryAfter; // seconds
    console.log(`Rate limited. Retry after ${retryAfter}s`);
    
    // Wait and retry
    await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
    return sdk.scores.getScore(address);
  }
}

Validation Errors

Code: INVALID_ADDRESS
Status: 400
The provided Ethereum address is invalid.
Code: INVALID_PARAMETERS
Status: 400
Request parameters are invalid or missing.
try {
  await sdk.achievements.claim({
    achievementId: '',  // Invalid
  });
} catch (error) {
  if (error.code === 'INVALID_PARAMETERS') {
    console.log('Validation errors:', error.details.errors);
    // {
    //   achievementId: 'Required field',
    //   proof: 'Invalid proof format'
    // }
  }
}
Code: INVALID_SCHEMA
Status: 400
Attestation schema is invalid or doesn’t exist.

Network Errors

Code: NETWORK_ERROR
Status: 500
Network connection failed.
Code: TIMEOUT
Status: 504
Request timed out.
Code: SERVICE_UNAVAILABLE
Status: 503
The service is temporarily unavailable.

Data Errors

Code: INSUFFICIENT_DATA
Status: 400
Not enough data to complete the operation.
try {
  const proof = await sdk.zkProofs.generate({
    address,
    type: 'trading-volume',
    minVolume: '100000',
  });
} catch (error) {
  if (error.code === 'INSUFFICIENT_DATA') {
    console.log('User needs more trading history');
  }
}
Code: SCORE_NOT_READY
Status: 202
Score is still being calculated.

Error Handling Patterns

Basic Try-Catch

async function getScoreSafely(address: string) {
  try {
    const score = await sdk.scores.getScore(address);
    return { success: true, data: score };
  } catch (error) {
    return {
      success: false,
      error: {
        code: error.code,
        message: error.message,
      },
    };
  }
}

Specific Error Handling

async function mintIdentityWithErrorHandling(address: string, username: string) {
  try {
    const identity = await sdk.identity.mint({ address, username });
    return { success: true, identity };
  } catch (error) {
    switch (error.code) {
      case 'USERNAME_TAKEN':
        return {
          success: false,
          error: 'This username is already taken. Please choose another.',
        };
      
      case 'INVALID_ADDRESS':
        return {
          success: false,
          error: 'Invalid Ethereum address provided.',
        };
      
      case 'IDENTITY_ALREADY_EXISTS':
        return {
          success: false,
          error: 'This address already has an identity.',
        };
      
      case 'RATE_LIMIT_EXCEEDED':
        return {
          success: false,
          error: 'Too many requests. Please try again later.',
        };
      
      default:
        return {
          success: false,
          error: 'An unexpected error occurred. Please try again.',
        };
    }
  }
}

Retry Logic

async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries: number = 3,
  delayMs: number = 1000
): Promise<T> {
  let lastError: any;
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      
      // Don't retry on certain errors
      if (['INVALID_API_KEY', 'INVALID_ADDRESS', 'USERNAME_TAKEN'].includes(error.code)) {
        throw error;
      }
      
      // Exponential backoff
      const delay = delayMs * Math.pow(2, i);
      console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw lastError;
}

// Usage
const score = await withRetry(() => sdk.scores.getScore(address));

Rate Limit Handler

class RateLimitHandler {
  private queue: Array<() => Promise<any>> = [];
  private processing = false;
  private lastRequest = 0;
  private minInterval = 1000; // 1 second between requests

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      this.queue.push(async () => {
        try {
          const result = await fn();
          resolve(result);
        } catch (error) {
          if (error.code === 'RATE_LIMIT_EXCEEDED') {
            // Re-queue the request
            this.queue.unshift(fn);
            const retryAfter = error.details.retryAfter * 1000;
            await new Promise(r => setTimeout(r, retryAfter));
          } else {
            reject(error);
          }
        }
      });

      if (!this.processing) {
        this.processQueue();
      }
    });
  }

  private async processQueue() {
    this.processing = true;

    while (this.queue.length > 0) {
      const now = Date.now();
      const timeSinceLastRequest = now - this.lastRequest;

      if (timeSinceLastRequest < this.minInterval) {
        await new Promise(resolve =>
          setTimeout(resolve, this.minInterval - timeSinceLastRequest)
        );
      }

      const fn = this.queue.shift();
      if (fn) {
        this.lastRequest = Date.now();
        await fn();
      }
    }

    this.processing = false;
  }
}

const rateLimiter = new RateLimitHandler();

// Usage
const score = await rateLimiter.execute(() => sdk.scores.getScore(address));

Circuit Breaker

class CircuitBreaker {
  private failureCount = 0;
  private successCount = 0;
  private lastFailureTime = 0;
  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';

  constructor(
    private threshold: number = 5,
    private timeout: number = 60000
  ) {}

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime > this.timeout) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess() {
    this.failureCount = 0;
    this.successCount++;

    if (this.state === 'HALF_OPEN' && this.successCount >= 2) {
      this.state = 'CLOSED';
      this.successCount = 0;
    }
  }

  private onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();

    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
    }
  }
}

const breaker = new CircuitBreaker();

// Usage
const score = await breaker.execute(() => sdk.scores.getScore(address));

Error Logging

class ErrorLogger {
  async logError(error: any, context: any = {}) {
    const errorLog = {
      code: error.code,
      message: error.message,
      statusCode: error.statusCode,
      timestamp: new Date().toISOString(),
      context,
      stack: error.stack,
    };

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

    // Could also send to external service
    // await sendToSentry(errorLog);
  }
}

const logger = new ErrorLogger();

// Usage
try {
  await sdk.scores.getScore(address);
} catch (error) {
  await logger.logError(error, {
    operation: 'getScore',
    address,
    userId: currentUser.id,
  });
  throw error;
}

Graceful Degradation

async function getScoreWithFallback(address: string) {
  try {
    return await sdk.scores.getScore(address);
  } catch (error) {
    if (error.code === 'SCORE_NOT_READY') {
      // Return estimated score
      return {
        overall: 500,
        breakdown: {},
        estimated: true,
        message: 'Score is being calculated',
      };
    }

    if (error.code === 'SERVICE_UNAVAILABLE') {
      // Return cached score if available
      const cached = await getCachedScore(address);
      if (cached) {
        return {
          ...cached,
          cached: true,
          message: 'Showing cached score',
        };
      }
    }

    // Re-throw if no fallback available
    throw error;
  }
}

Error Boundaries (React)

import { Component, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error?: any;
}

class ZKScoreErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: any) {
    return {
      hasError: true,
      error,
    };
  }

  componentDidCatch(error: any, errorInfo: any) {
    console.error('ZKScore Error:', error, errorInfo);
    
    // Log to error tracking service
    // logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="error-container">
          <h2>Something went wrong</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false })}>
            Try Again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Usage
<ZKScoreErrorBoundary>
  <Dashboard />
</ZKScoreErrorBoundary>

Validation Helper

function validateAddress(address: string): void {
  if (!address || typeof address !== 'string') {
    throw {
      code: 'INVALID_ADDRESS',
      message: 'Address is required',
      statusCode: 400,
    };
  }

  if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
    throw {
      code: 'INVALID_ADDRESS',
      message: 'Invalid Ethereum address format',
      statusCode: 400,
    };
  }
}

// Usage
try {
  validateAddress(userInput);
  const score = await sdk.scores.getScore(userInput);
} catch (error) {
  // Handle validation error
}

TypeScript Error Types

interface ZKScoreError {
  code: string;
  message: string;
  details?: Record<string, any>;
  statusCode: number;
  timestamp: string;
}

type ErrorCode =
  | 'INVALID_API_KEY'
  | 'API_KEY_EXPIRED'
  | 'UNAUTHORIZED'
  | 'IDENTITY_NOT_FOUND'
  | 'USERNAME_TAKEN'
  | 'RATE_LIMIT_EXCEEDED'
  | 'INVALID_ADDRESS'
  | 'INVALID_PARAMETERS'
  | 'NETWORK_ERROR'
  | 'TIMEOUT'
  | 'SERVICE_UNAVAILABLE'
  | 'INSUFFICIENT_DATA'
  | 'SCORE_NOT_READY';

Best Practices

Always Handle Errors

Wrap all SDK calls in try-catch blocks

Provide Feedback

Show user-friendly error messages

Log Errors

Track errors for debugging and monitoring

Implement Retries

Retry transient failures with backoff

Validate Input

Validate before making API calls

Graceful Degradation

Provide fallbacks when possible

Next Steps