GraphQL Error Handling

Crane Ledger's GraphQL API provides comprehensive error handling with specific error codes, detailed messages, and structured error responses. Understanding these errors helps you build robust applications that gracefully handle edge cases and business logic violations.

Error Response Structure

GraphQL errors follow the standard GraphQL specification with Crane Ledger-specific extensions:

{
  "errors": [
    {
      "message": "Human-readable error description",
      "locations": [
        {
          "line": 3,
          "column": 7
        }
      ],
      "path": ["organization", "accounts", 0, "balance"],
      "extensions": {
        "code": "ERROR_CODE",
        "details": {
          "field": "accountId",
          "value": "invalid_id",
          "expected": "act_xxxxxxxx"
        },
        "timestamp": "2024-01-15T10:30:00Z",
        "requestId": "req_1234567890abcdef"
      }
    }
  ]
}

Error Categories

Authentication Errors

UNAUTHENTICATED

HTTP Status: 401 Description: Missing or invalid API key

{
  "errors": [
    {
      "message": "Authentication required",
      "extensions": {
        "code": "UNAUTHENTICATED"
      }
    }
  ]
}

Causes:

  • Missing Authorization header
  • Invalid API key format
  • Expired API key
  • Disabled API key

Resolution:

// Ensure proper authentication
const headers = {
  'Authorization': 'Bearer cl_live_your_api_key_here'
};

FORBIDDEN

HTTP Status: 403 Description: API key doesn't have permission for the operation

{
  "errors": [
    {
      "message": "Access denied to organization",
      "extensions": {
        "code": "FORBIDDEN",
        "details": {
          "organizationId": "org_1234567890abcdef",
          "apiKeyId": "key_1234567890abcdef"
        }
      }
    }
  ]
}

Causes:

  • API key belongs to different organization
  • API key lacks required permissions
  • Organization access revoked

Validation Errors

GRAPHQL_VALIDATION_FAILED

HTTP Status: 400 Description: Invalid GraphQL query syntax or structure

{
  "errors": [
    {
      "message": "Field 'invalidField' is not defined on type 'OrganizationNode'",
      "locations": [{ "line": 6, "column": 9 }],
      "extensions": {
        "code": "GRAPHQL_VALIDATION_FAILED"
      }
    }
  ]
}

Causes:

  • Typo in field name
  • Invalid argument type
  • Missing required arguments
  • Unknown type references

VALIDATION_ERROR

HTTP Status: 400 Description: Business data validation failed

{
  "errors": [
    {
      "message": "Account code '1001' already exists",
      "extensions": {
        "code": "VALIDATION_ERROR",
        "details": {
          "field": "code",
          "value": "1001",
          "constraint": "unique_within_organization"
        }
      }
    }
  ]
}

Common Validation Errors:

  • Duplicate account codes
  • Invalid email formats
  • Negative amounts where not allowed
  • Future dates where not permitted
  • Invalid currency codes

Business Logic Errors

BUSINESS_RULE_VIOLATION

HTTP Status: 400 Description: Operation violates accounting business rules

{
  "errors": [
    {
      "message": "Transaction does not balance: debits (150.00) != credits (100.00)",
      "extensions": {
        "code": "BUSINESS_RULE_VIOLATION",
        "details": {
          "totalDebits": 150.00,
          "totalCredits": 100.00,
          "difference": 50.00
        }
      }
    }
  ]
}

Common Business Rule Violations:

  • Transaction balancing: Debits must equal credits
  • Account type rules: Debits/credits must follow account type conventions
  • Date constraints: Posting dates can't be in the future
  • Amount limits: Negative amounts where not allowed
  • Reference integrity: Referenced entities must exist

INSUFFICIENT_FUNDS

HTTP Status: 402 Description: Operation would result in negative balance

{
  "errors": [
    {
      "message": "Insufficient funds in account",
      "extensions": {
        "code": "INSUFFICIENT_FUNDS",
        "details": {
          "accountId": "act_1001",
          "currentBalance": 500.00,
          "requestedAmount": 750.00,
          "shortfall": 250.00
        }
      }
    }
  ]
}

Credit and Billing Errors

CREDIT_LIMIT_EXCEEDED

HTTP Status: 402 Description: Insufficient credits for operation

{
  "errors": [
    {
      "message": "Insufficient credits remaining (required: 5, available: 2)",
      "extensions": {
        "code": "CREDIT_LIMIT_EXCEEDED",
        "details": {
          "required": 5,
          "available": 2,
          "shortfall": 3,
          "autoRechargeEnabled": true
        }
      }
    }
  ]
}

Resolution:

// Check credits before operations
const { data } = await client.query({
  query: gql`query { organization { creditsRemaining } }`
});

if (data.organization.creditsRemaining < requiredCredits) {
  // Purchase more credits or enable auto-recharge
  await purchaseCredits();
}

BILLING_ERROR

HTTP Status: 402 Description: Billing operation failed

{
  "errors": [
    {
      "message": "Credit card payment failed",
      "extensions": {
        "code": "BILLING_ERROR",
        "details": {
          "paymentMethod": "card",
          "errorCode": "card_declined",
          "errorMessage": "Your card was declined"
        }
      }
    }
  ]
}

Data Access Errors

NOT_FOUND

HTTP Status: 404 Description: Requested resource doesn't exist

{
  "errors": [
    {
      "message": "Account not found: act_invalid_id",
      "extensions": {
        "code": "NOT_FOUND",
        "details": {
          "entityType": "account",
          "entityId": "act_invalid_id"
        }
      }
    }
  ]
}

Common Not Found Errors:

  • Invalid account IDs
  • Non-existent transaction IDs
  • Missing contact records
  • Invalid invoice/bill numbers

System Errors

INTERNAL_SERVER_ERROR

HTTP Status: 500 Description: Unexpected server error

{
  "errors": [
    {
      "message": "Internal server error",
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "requestId": "req_1234567890abcdef"
      }
    }
  ]
}

When you see this:

  • Contact support with the requestId
  • Include the full query/mutation that caused the error
  • Provide timestamp and user context

SERVICE_UNAVAILABLE

HTTP Status: 503 Description: Service temporarily unavailable

{
  "errors": [
    {
      "message": "Service temporarily unavailable",
      "extensions": {
        "code": "SERVICE_UNAVAILABLE",
        "retryAfter": 30
      }
    }
  ]
}

Rate Limiting Errors

RATE_LIMIT_EXCEEDED

HTTP Status: 429 Description: Too many requests

{
  "errors": [
    {
      "message": "Rate limit exceeded",
      "extensions": {
        "code": "RATE_LIMIT_EXCEEDED",
        "retryAfter": 60,
        "limit": 1000,
        "remaining": 0,
        "resetTime": "2024-01-15T11:00:00Z"
      }
    }
  ]
}

Rate Limits:

  • Live Keys: 1,000 requests per minute
  • Test Keys: 100 requests per minute

Resolution:

// Implement exponential backoff
async function makeRequestWithRetry(query, variables, maxRetries = 3) {
  let attempt = 0;

  while (attempt < maxRetries) {
    try {
      return await client.request(query, variables);
    } catch (error) {
      if (error.response?.status === 429) {
        const retryAfter = error.response.headers['retry-after'] || 60;
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
        attempt++;
      } else {
        throw error;
      }
    }
  }

  throw new Error('Max retries exceeded');
}

Error Handling Strategies

Client-Side Error Handling

JavaScript/React Error Handling

import { ApolloClient, InMemoryCache, from } from '@apollo/client';
import { onError } from '@apollo/client/link/error';

const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, extensions }) => {
      const errorCode = extensions?.code;

      switch (errorCode) {
        case 'UNAUTHENTICATED':
          // Redirect to login
          window.location.href = '/login';
          break;

        case 'CREDIT_LIMIT_EXCEEDED':
          // Show credit purchase modal
          showCreditPurchaseModal();
          break;

        case 'BUSINESS_RULE_VIOLATION':
          // Show user-friendly message
          showErrorToast('Transaction must balance (debits = credits)');
          break;

        case 'RATE_LIMIT_EXCEEDED':
          // Retry with backoff
          const retryAfter = extensions?.retryAfter || 60;
          setTimeout(() => forward(operation), retryAfter * 1000);
          break;

        default:
          // Log and show generic error
          console.error('GraphQL error:', message);
          showErrorToast('An error occurred. Please try again.');
      }
    });
  }

  if (networkError) {
    console.error('Network error:', networkError);
    showErrorToast('Network error. Please check your connection.');
  }
});

const client = new ApolloClient({
  link: from([errorLink, httpLink]),
  cache: new InMemoryCache()
});

Python Error Handling

from gql import gql, Client
from gql.transport.exceptions import TransportQueryError

async def safe_graphql_call(client, query, variables=None):
    try:
        return await client.execute_async(query, variable_values=variables)
    except TransportQueryError as e:
        for error in e.errors:
            error_code = error.get('extensions', {}).get('code')

            if error_code == 'UNAUTHENTICATED':
                raise AuthenticationError("Please log in again")
            elif error_code == 'CREDIT_LIMIT_EXCEEDED':
                raise CreditLimitError("Insufficient credits")
            elif error_code == 'BUSINESS_RULE_VIOLATION':
                raise ValidationError(error['message'])
            elif error_code == 'RATE_LIMIT_EXCEEDED':
                retry_after = error.get('extensions', {}).get('retryAfter', 60)
                await asyncio.sleep(retry_after)
                return await safe_graphql_call(client, query, variables)
            else:
                raise APIError(f"API Error: {error['message']}")

        raise APIError("Unknown GraphQL error")

Error Recovery Patterns

Automatic Retry Logic

class GraphQLClientWithRetry {
  constructor(endpoint, authToken) {
    this.endpoint = endpoint;
    this.authToken = authToken;
    this.maxRetries = 3;
  }

  async request(query, variables = {}) {
    let lastError;

    for (let attempt = 0; attempt < this.maxRetries; attempt++) {
      try {
        return await this._executeQuery(query, variables);
      } catch (error) {
        lastError = error;

        // Don't retry certain errors
        if (this._isNonRetryableError(error)) {
          break;
        }

        // Calculate backoff delay
        const delay = this._calculateBackoffDelay(attempt, error);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }

    throw lastError;
  }

  _isNonRetryableError(error) {
    const nonRetryableCodes = [
      'UNAUTHENTICATED',
      'FORBIDDEN',
      'VALIDATION_ERROR',
      'BUSINESS_RULE_VIOLATION',
      'NOT_FOUND'
    ];

    return nonRetryableCodes.includes(error.extensions?.code);
  }

  _calculateBackoffDelay(attempt, error) {
    // Use retry-after header if provided
    if (error.extensions?.retryAfter) {
      return error.extensions.retryAfter * 1000;
    }

    // Exponential backoff: 1s, 2s, 4s, etc.
    return Math.pow(2, attempt) * 1000;
  }
}

Credit Management

class CreditAwareClient {
  constructor(graphqlClient) {
    this.client = graphqlClient;
    this.minCredits = 10; // Keep at least 10 credits
  }

  async ensureCredits(operationCost) {
    const { data } = await this.client.query({
      query: gql`query { organization { creditsRemaining autoRechargeEnabled } }`
    });

    const remaining = data.organization.creditsRemaining;
    const autoRecharge = data.organization.autoRechargeEnabled;

    if (remaining < this.minCredits + operationCost) {
      if (autoRecharge) {
        // Wait for auto-recharge to complete
        console.log('Waiting for auto-recharge...');
        await this._waitForCreditRecharge(operationCost);
      } else {
        // Trigger manual credit purchase
        await this._purchaseCredits(operationCost);
      }
    }
  }

  async executeWithCreditCheck(operation, cost) {
    await this.ensureCredits(cost);
    return await this.client.mutate(operation);
  }
}

User-Friendly Error Messages

const ERROR_MESSAGES = {
  UNAUTHENTICATED: 'Please log in to continue',
  FORBIDDEN: 'You don\'t have permission to perform this action',
  CREDIT_LIMIT_EXCEEDED: 'You\'ve run out of credits. Please add more to continue',
  BUSINESS_RULE_VIOLATION: 'This action violates accounting rules. Please check your entries',
  VALIDATION_ERROR: 'Please check your input and try again',
  RATE_LIMIT_EXCEEDED: 'Too many requests. Please wait a moment and try again',
  NOT_FOUND: 'The requested item could not be found',
  INTERNAL_SERVER_ERROR: 'Something went wrong on our end. Please try again later'
};

function getUserFriendlyMessage(errorCode, details = {}) {
  let message = ERROR_MESSAGES[errorCode] || 'An unexpected error occurred';

  // Add specific details where helpful
  if (errorCode === 'CREDIT_LIMIT_EXCEEDED') {
    message += ` (Need ${details.shortfall} more credits)`;
  }

  return message;
}

Debugging Errors

Request ID Tracking

Every error includes a requestId for debugging:

// Log errors with request ID for support
try {
  const result = await client.mutate({ mutation, variables });
} catch (error) {
  const requestId = error.extensions?.requestId;
  console.error(`Request ${requestId} failed:`, error.message);

  // Report to error tracking service
  errorTracker.captureException(error, {
    tags: { requestId },
    extra: { query: mutation.loc.source.body, variables }
  });
}

GraphQL Playground Debugging

Use the GraphQL playground for testing:

  1. Open Playground: https://api.craneledger.ai/graphql/playground
  2. Add Authentication: Include Authorization header
  3. Test Queries: Run queries to see exact error responses
  4. Check Schema: Use introspection to verify field availability
  5. Monitor Network: Check request/response details

Common Debugging Steps

  1. Verify Authentication:
curl -H "Authorization: Bearer cl_live_your_key" \
     https://api.craneledger.ai/graphql \
     -d '{"query": "query { organization { id } }"}'
  1. Check Credits:
curl -H "Authorization: Bearer cl_live_your_key" \
     https://api.craneledger.ai/graphql \
     -d '{"query": "query { organization { creditsRemaining } }"}'
  1. Test Simple Query:
{
  "query": "query { __typename }"
}
  1. Validate Schema:
query CheckSchema {
  __schema {
    queryType {
      name
    }
    mutationType {
      name
    }
  }
}

Error Prevention

Input Validation

// Client-side validation before sending
function validateTransactionInput(entries) {
  const errors = [];

  // Check for required fields
  entries.forEach((entry, index) => {
    if (!entry.accountId) {
      errors.push(`Entry ${index + 1}: Missing account ID`);
    }
    if (!entry.entryType || !['DEBIT', 'CREDIT'].includes(entry.entryType)) {
      errors.push(`Entry ${index + 1}: Invalid entry type`);
    }
    if (!entry.amount || entry.amount <= 0) {
      errors.push(`Entry ${index + 1}: Invalid amount`);
    }
  });

  // Check balance
  const totalDebits = entries
    .filter(e => e.entryType === 'DEBIT')
    .reduce((sum, e) => sum + e.amount, 0);

  const totalCredits = entries
    .filter(e => e.entryType === 'CREDIT')
    .reduce((sum, e) => sum + e.amount, 0);

  if (totalDebits !== totalCredits) {
    errors.push(`Transaction unbalanced: Debits (${totalDebits}) ≠ Credits (${totalCredits})`);
  }

  return errors;
}

Type Safety

// Use TypeScript for compile-time error prevention
interface TransactionEntry {
  accountId: string;
  entryType: 'DEBIT' | 'CREDIT';
  amount: number;
  description?: string;
  reference?: string;
}

interface CreateTransactionInput {
  description: string;
  date: string;
  entries: TransactionEntry[];
}

function createTransaction(input: CreateTransactionInput) {
  // TypeScript ensures correct structure
  return client.mutate({
    mutation: CREATE_TRANSACTION,
    variables: { input }
  });
}

Best Practices

Error Handling Architecture

  1. Centralized Error Handler:
class GraphQLErrorHandler {
  static handle(error) {
    const userMessage = this.getUserMessage(error);
    const logData = this.getLogData(error);
    const recoveryAction = this.getRecoveryAction(error);

    // Log for debugging
    console.error('GraphQL Error:', logData);

    // Show user-friendly message
    this.showUserNotification(userMessage);

    // Attempt recovery if possible
    if (recoveryAction) {
      recoveryAction();
    }

    return { userMessage, recoveryAction };
  }
}
  1. Error Boundaries (React):
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

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

  componentDidCatch(error, errorInfo) {
    // Log error details
    errorTracker.captureException(error, { extra: errorInfo });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <p>Please refresh the page and try again</p>
          <button onClick={() => window.location.reload()}>
            Refresh Page
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}
  1. Monitoring and Alerting:
// Monitor error rates
class ErrorMonitor {
  constructor() {
    this.errors = new Map();
    this.thresholds = {
      'CREDIT_LIMIT_EXCEEDED': 10,  // Alert if >10 in 5 minutes
      'RATE_LIMIT_EXCEEDED': 5,
      'INTERNAL_SERVER_ERROR': 1
    };
  }

  trackError(errorCode) {
    const count = this.errors.get(errorCode) || 0;
    this.errors.set(errorCode, count + 1);

    if (count + 1 > this.thresholds[errorCode]) {
      this.alert(errorCode, count + 1);
    }
  }

  alert(errorCode, count) {
    // Send alert to monitoring service
    monitoring.alert(`High error rate: ${errorCode} (${count} occurrences)`);
  }
}

Understanding and properly handling errors in Crane Ledger's GraphQL API ensures your application provides a reliable, user-friendly experience. The structured error responses with specific codes and detailed messages help you implement appropriate error handling and recovery strategies.


Need help?

Create a free account to access our support portal. Once signed in, use the Support tab in your dashboard to submit a support ticket — our team typically responds within 24 hours.