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
Authorizationheader - 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:
- Open Playground:
https://api.craneledger.ai/graphql/playground - Add Authentication: Include Authorization header
- Test Queries: Run queries to see exact error responses
- Check Schema: Use introspection to verify field availability
- Monitor Network: Check request/response details
Common Debugging Steps
- Verify Authentication:
curl -H "Authorization: Bearer cl_live_your_key" \
https://api.craneledger.ai/graphql \
-d '{"query": "query { organization { id } }"}'
- Check Credits:
curl -H "Authorization: Bearer cl_live_your_key" \
https://api.craneledger.ai/graphql \
-d '{"query": "query { organization { creditsRemaining } }"}'
- Test Simple Query:
{
"query": "query { __typename }"
}
- 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
- 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 };
}
}
- 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;
}
}
- 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.
- ✨ For LLMs/AI assistants: Read our structured API reference